Repository: metallb/metallb
Pull request #2292 (2024-02-16 15:07:19)

Communities / LocalPref routemaps: create one route map per neigh
The current implementation creates one route map per localpref / community, duplicating the same route-map for each advertisement (while what we really need is one route map, many prefix-lists entries, one per service).

Here we collect all the communities / localpref / large communities per neighbor, and we use it to generate a route-map per neighbor per community / localpref only once.


Patch:
diff --git a/charts/metallb/README.md b/charts/metallb/README.md
index 483eb921..929f7a93 100644
--- a/charts/metallb/README.md
+++ b/charts/metallb/README.md
@@ -124,6 +124,7 @@ Kubernetes: `>= 1.19.0-0`
 | speaker.frr.metricsPort | int | `7473` |  |
 | speaker.frr.resources | object | `{}` |  |
 | speaker.frrMetrics.resources | object | `{}` |  |
+| speaker.ignoreExcludeLB | bool | `false` |  |
 | speaker.image.pullPolicy | string | `nil` |  |
 | speaker.image.repository | string | `"quay.io/metallb/speaker"` |  |
 | speaker.image.tag | string | `nil` |  |
diff --git a/charts/metallb/templates/speaker.yaml b/charts/metallb/templates/speaker.yaml
index f7b33e25..f7e9dfe0 100644
--- a/charts/metallb/templates/speaker.yaml
+++ b/charts/metallb/templates/speaker.yaml
@@ -244,6 +244,9 @@ spec:
         {{- if .Values.speaker.wanConfig }}
         - --ml-wan-config
         {{- end }}
+        {{- if .Values.speaker.ignoreExcludeLB}}
+        - --ignore-exclude-lb
+        {{- end }}
         env:
         - name: METALLB_NODE_NAME
           valueFrom:
@@ -330,7 +333,7 @@ spec:
         {{- if or .Values.speaker.frr.enabled .Values.speaker.memberlist.enabled .Values.speaker.excludeInterfaces.enabled }}
         volumeMounts:
           {{- if .Values.speaker.memberlist.enabled }}
-          - name: memberlist 
+          - name: memberlist
             mountPath: {{ .Values.speaker.memberlist.mlSecretKeyPath }}
           {{- end }}
           {{- if .Values.speaker.frr.enabled }}
@@ -525,7 +528,7 @@ spec:
         operator: Exists
       - key: node-role.kubernetes.io/control-plane
         effect: NoSchedule
-        operator: Exists        
+        operator: Exists
       {{- end }}
       {{- with .Values.speaker.tolerations }}
         {{- toYaml . | nindent 6 }}
diff --git a/charts/metallb/values.schema.json b/charts/metallb/values.schema.json
index b6373532..bc0dd840 100644
--- a/charts/metallb/values.schema.json
+++ b/charts/metallb/values.schema.json
@@ -299,7 +299,7 @@
       },
       "required": [ "podMonitor", "prometheusRule" ]
     },
-    "controller": { 
+    "controller": {
       "allOf": [
         { "$ref": "#/definitions/component" },
         { "description": "MetalLB Controller",
@@ -330,7 +330,7 @@
         }
       ]
     },
-    "speaker": { 
+    "speaker": {
       "allOf": [
         { "$ref": "#/definitions/component" },
         { "description": "MetalLB Speaker",
@@ -364,6 +364,9 @@
                 }
               }
             },
+            "ignoreExcludeLB": {
+              "type": "boolean"
+            },
             "updateStrategy": {
               "type": "object",
               "properties": {
diff --git a/charts/metallb/values.yaml b/charts/metallb/values.yaml
index d9ec7989..738f25f8 100644
--- a/charts/metallb/values.yaml
+++ b/charts/metallb/values.yaml
@@ -268,6 +268,9 @@ speaker:
     mlSecretKeyPath: "/etc/ml_secret_key"
   excludeInterfaces:
     enabled: true
+  # ignore the exclude-from-external-loadbalancer label
+  ignoreExcludeLB: false
+
   image:
     repository: quay.io/metallb/speaker
     tag:
diff --git a/internal/bgp/frr/config.go b/internal/bgp/frr/config.go
index a3e2ec35..69c4428f 100644
--- a/internal/bgp/frr/config.go
+++ b/internal/bgp/frr/config.go
@@ -79,6 +79,15 @@ type neighborConfig struct {
 	VRFName             string
 	HasV4Advertisements bool
 	HasV6Advertisements bool
+	// It has at least one advertisement with these communities
+	CommunitiesV4 []string
+	CommunitiesV6 []string
+	// It has at least one advertisement with these large communities
+	LargeCommunitiesV4 []string
+	LargeCommunitiesV6 []string
+	// It has at least one advertisement with these local preferences
+	LocalPrefsV4 []uint32
+	LocalPrefsV6 []uint32
 }
 
 func (n *neighborConfig) ID() string {
diff --git a/internal/bgp/frr/frr.go b/internal/bgp/frr/frr.go
index fe55c97a..132b3d06 100644
--- a/internal/bgp/frr/frr.go
+++ b/internal/bgp/frr/frr.go
@@ -201,13 +201,26 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 		ExtraConfig: sm.extraConfig,
 	}
 
+	type neighborProperties struct {
+		// It has at least one advertisement with these communities
+		CommunitiesV4 sets.Set[string]
+		CommunitiesV6 sets.Set[string]
+		// It has at least one advertisement with these large communities
+		LargeCommunitiesV4 sets.Set[string]
+		LargeCommunitiesV6 sets.Set[string]
+		// It has at least one advertisement with these local preferences
+		LocalPrefsV4 sets.Set[uint32]
+		LocalPrefsV6 sets.Set[uint32]
+	}
+
 	type router struct {
-		myASN        uint32
-		routerID     string
-		neighbors    map[string]*neighborConfig
-		vrf          string
-		ipV4Prefixes map[string]string
-		ipV6Prefixes map[string]string
+		myASN               uint32
+		routerID            string
+		neighbors           map[string]*neighborConfig
+		neighborsProperties map[string]*neighborProperties
+		vrf                 string
+		ipV4Prefixes        map[string]string
+		ipV6Prefixes        map[string]string
 	}
 
 	routers := make(map[string]*router)
@@ -226,11 +239,12 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 		routerName := RouterName(s.RouterID.String(), s.MyASN, s.VRFName)
 		if rout, exist = routers[routerName]; !exist {
 			rout = &router{
-				myASN:        s.MyASN,
-				neighbors:    make(map[string]*neighborConfig),
-				ipV4Prefixes: make(map[string]string),
-				ipV6Prefixes: make(map[string]string),
-				vrf:          s.VRFName,
+				myASN:               s.MyASN,
+				neighbors:           make(map[string]*neighborConfig),
+				neighborsProperties: make(map[string]*neighborProperties),
+				ipV4Prefixes:        make(map[string]string),
+				ipV6Prefixes:        make(map[string]string),
+				vrf:                 s.VRFName,
 			}
 			if s.RouterID != nil {
 				rout.routerID = s.RouterID.String()
@@ -258,6 +272,7 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 			}
 
 			neighbor = &neighborConfig{
+				Name:           neighborName,
 				IPFamily:       family,
 				ASN:            s.PeerASN,
 				Addr:           host,
@@ -275,8 +290,18 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 				neighbor.SrcAddr = s.SourceAddress.String()
 			}
 			rout.neighbors[neighborName] = neighbor
+			rout.neighborsProperties[neighborName] = &neighborProperties{
+				CommunitiesV4:      sets.New[string](),
+				CommunitiesV6:      sets.New[string](),
+				LargeCommunitiesV4: sets.New[string](),
+				LargeCommunitiesV6: sets.New[string](),
+				LocalPrefsV4:       sets.New[uint32](),
+				LocalPrefsV6:       sets.New[uint32](),
+			}
 		}
 
+		properties := rout.neighborsProperties[neighborName]
+
 		for _, adv := range s.advertised {
 			family := ipfamily.ForAddress(adv.Prefix.IP)
 
@@ -287,9 +312,19 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 			for _, c := range adv.Communities {
 				if community.IsLarge(c) {
 					largeCommunities = append(largeCommunities, c.String())
+					if family == ipfamily.IPv4 {
+						properties.LargeCommunitiesV4.Insert(c.String())
+						continue
+					}
+					properties.LargeCommunitiesV6.Insert(c.String())
 					continue
 				}
 				communities = append(communities, c.String())
+				if family == ipfamily.IPv4 {
+					properties.CommunitiesV4.Insert(c.String())
+					continue
+				}
+				properties.CommunitiesV6.Insert(c.String())
 			}
 
 			prefix := adv.Prefix.String()
@@ -300,6 +335,13 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 				LargeCommunities: sort.StringSlice(largeCommunities),
 				LocalPref:        adv.LocalPref,
 			}
+			if adv.LocalPref != 0 {
+				if family == ipfamily.IPv4 {
+					properties.LocalPrefsV4.Insert(adv.LocalPref)
+				} else {
+					properties.LocalPrefsV6.Insert(adv.LocalPref)
+				}
+			}
 
 			neighbor.Advertisements, err = addToAdvertisements(neighbor.Advertisements, &advConfig)
 			if err != nil {
@@ -326,6 +368,17 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 			IPV4Prefixes: sortMap(r.ipV4Prefixes),
 			IPV6Prefixes: sortMap(r.ipV6Prefixes),
 		}
+		// Filling in the neighbor properties
+		for _, n := range toAdd.Neighbors {
+			properties := r.neighborsProperties[n.Name]
+			n.CommunitiesV4 = sets.List(properties.CommunitiesV4)
+			n.CommunitiesV6 = sets.List(properties.CommunitiesV6)
+			n.LargeCommunitiesV4 = sets.List(properties.LargeCommunitiesV4)
+			n.LargeCommunitiesV6 = sets.List(properties.LargeCommunitiesV6)
+			n.LocalPrefsV4 = sets.List(properties.LocalPrefsV4)
+			n.LocalPrefsV6 = sets.List(properties.LocalPrefsV6)
+		}
+
 		config.Routers = append(config.Routers, toAdd)
 	}
 	return config, nil
diff --git a/internal/bgp/frr/templates/filters.tmpl b/internal/bgp/frr/templates/filters.tmpl
index ce3dd86e..0430e3d0 100644
--- a/internal/bgp/frr/templates/filters.tmpl
+++ b/internal/bgp/frr/templates/filters.tmpl
@@ -1,27 +1,16 @@
 {{- define "localpreffilter" -}}
-{{$localPrefixListName :=localPrefPrefixList .neighbor .advertisement.LocalPref}}
+{{$localPrefixListName :=localPrefPrefixList .neighbor .advertisement.LocalPref -}}
 {{frrIPFamily .advertisement.IPFamily}} prefix-list {{$localPrefixListName}} seq {{counter $localPrefixListName}} permit {{.advertisement.Prefix}}
-route-map {{.neighbor.ID}}-out permit {{counter .neighbor.ID}}
-  match {{frrIPFamily .advertisement.IPFamily}} address prefix-list {{localPrefPrefixList .neighbor .advertisement.LocalPref}}
-  set local-preference {{.advertisement.LocalPref}}
-  on-match next
 {{- end -}}
 
 {{- define "communityfilter" -}}
-{{$communityPrefixlistName :=communityPrefixList .neighbor .community}}
+{{$communityPrefixlistName :=communityPrefixList .neighbor .community -}}
 {{frrIPFamily .advertisement.IPFamily}} prefix-list {{$communityPrefixlistName}} seq {{counter $communityPrefixlistName}} permit {{.advertisement.Prefix}}
-route-map {{.neighbor.ID}}-out permit {{counter .neighbor.ID}}
-  match {{frrIPFamily .advertisement.IPFamily}} address prefix-list {{communityPrefixList .neighbor .community}}
-  set community {{.community}} additive
-  on-match next
 {{- end -}}
 
 {{- define "largecommunityfilter" -}}
-{{frrIPFamily .advertisement.IPFamily}} prefix-list {{largeCommunityPrefixList .neighbor .largecommunity}} permit {{.advertisement.Prefix}}
-route-map {{.neighbor.ID}}-out permit {{counter .neighbor.ID}}
-  match {{frrIPFamily .advertisement.IPFamily}} address prefix-list {{largeCommunityPrefixList .neighbor .largecommunity}}
-  set large-community {{.largecommunity}} additive
-  on-match next
+{{$largeCommunityPrefixlistName :=largeCommunityPrefixList .neighbor .largecommunity -}}
+{{frrIPFamily .advertisement.IPFamily}} prefix-list {{largeCommunityPrefixList .neighbor .largecommunity}} seq {{counter $largeCommunityPrefixlistName}} permit {{.advertisement.Prefix}}
 {{- end -}}
 
 {{- /* The prefixes are per router in FRR, but MetalLB api allows to associate a given BGPAdvertisement to a service IP,
@@ -31,6 +20,49 @@ route-map {{.neighbor.ID}}-out permit {{counter .neighbor.ID}}
 {{- define "neighborfilters" -}}
 
 route-map {{.neighbor.ID}}-in deny 20
+
+{{- range $c := .neighbor.LocalPrefsV4 }}
+route-map {{$.neighbor.ID}}-out permit {{counter $.neighbor.ID}}
+  match ip address prefix-list {{localPrefPrefixList $.neighbor . }}
+  set local-preference {{.}}
+  on-match next
+{{- end -}}
+
+{{- range $c := .neighbor.LocalPrefsV6 }}
+route-map {{$.neighbor.ID}}-out permit {{counter $.neighbor.ID}}
+  match ipv6 address prefix-list {{localPrefPrefixList $.neighbor . }}
+  set local-preference {{.}}
+  on-match next
+{{- end -}}
+
+{{- range $c := .neighbor.LargeCommunitiesV4 }}
+route-map {{$.neighbor.ID}}-out permit {{counter $.neighbor.ID}}
+  match ip address prefix-list {{largeCommunityPrefixList $.neighbor .}}
+  set large-community {{.}} additive
+  on-match next
+{{- end -}}
+
+{{- range $c := .neighbor.LargeCommunitiesV6 }}
+route-map {{$.neighbor.ID}}-out permit {{counter $.neighbor.ID}}
+  match ipv6 address prefix-list {{largeCommunityPrefixList $.neighbor .}}
+  set large-community {{.}} additive
+  on-match next
+{{- end -}}
+
+{{- range $c := .neighbor.CommunitiesV4 }}
+route-map {{$.neighbor.ID}}-out permit {{counter $.neighbor.ID}}
+  match ip address prefix-list {{communityPrefixList $.neighbor .}}
+  set community {{.}} additive
+  on-match next
+{{- end -}}
+
+{{- range $c := .neighbor.CommunitiesV6 }}
+route-map {{$.neighbor.ID}}-out permit {{counter $.neighbor.ID}}
+  match ipv6 address prefix-list {{communityPrefixList $.neighbor .}}
+  set community {{.}} additive
+  on-match next
+{{- end -}}
+
 {{- range $a := .neighbor.Advertisements }}
 {{/* Advertisements for which we must enable set the local pref */}}
 {{- if not (eq $a.LocalPref 0)}}
@@ -46,7 +78,7 @@ route-map {{.neighbor.ID}}-in deny 20
 {{- end }}
 {{/* this advertisement is allowed to the specific neighbor  */}}
 {{$plistName:=allowedPrefixList $.neighbor}}
- {{frrIPFamily $a.IPFamily}} prefix-list {{$plistName}} seq {{counter $plistName}} permit {{$a.Prefix}}
+{{frrIPFamily $a.IPFamily}} prefix-list {{$plistName}} seq {{counter $plistName}} permit {{$a.Prefix}}
 {{- end }}
 
 
diff --git a/speaker/bgp_controller.go b/speaker/bgp_controller.go
index 314bee19..741664ab 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -51,13 +51,14 @@ type peer struct {
 }
 
 type bgpController struct {
-	logger         log.Logger
-	myNode         string
-	nodeLabels     labels.Set
-	peers          []*peer
-	svcAds         map[string][]*bgp.Advertisement
-	bgpType        bgpImplementation
-	sessionManager bgp.SessionManager
+	logger          log.Logger
+	myNode          string
+	nodeLabels      labels.Set
+	peers           []*peer
+	svcAds          map[string][]*bgp.Advertisement
+	bgpType         bgpImplementation
+	sessionManager  bgp.SessionManager
+	ignoreExcludeLB bool
 }
 
 func (c *bgpController) SetConfig(l log.Logger, cfg *config.Config) error {
@@ -157,7 +158,7 @@ func (c *bgpController) ShouldAnnounce(l log.Logger, name string, _ []net.IP, po
 		return "nodeNetworkUnavailable"
 	}
 
-	if k8snodes.IsNodeExcludedFromBalancers(nodes[c.myNode]) {
+	if !c.ignoreExcludeLB && k8snodes.IsNodeExcludedFromBalancers(nodes[c.myNode]) {
 		level.Debug(l).Log("event", "skipping should announce bgp", "service", name, "reason", "speaker's node has labeled 'node.kubernetes.io/exclude-from-external-load-balancers'")
 		return "nodeLabeledExcludeBalancers"
 	}
diff --git a/speaker/layer2_controller.go b/speaker/layer2_controller.go
index a572873c..778c875f 100644
--- a/speaker/layer2_controller.go
+++ b/speaker/layer2_controller.go
@@ -32,9 +32,10 @@ import (
 )
 
 type layer2Controller struct {
-	announcer *layer2.Announce
-	myNode    string
-	sList     SpeakerList
+	announcer       *layer2.Announce
+	myNode          string
+	ignoreExcludeLB bool
+	sList           SpeakerList
 }
 
 func (c *layer2Controller) SetConfig(log.Logger, *config.Config) error {
@@ -89,7 +90,7 @@ func (c *layer2Controller) ShouldAnnounce(l log.Logger, name string, toAnnounce
 	}
 
 	// we select the nodes with at least one matching l2 advertisement
-	forPool := speakersForPool(c.sList.UsableSpeakers(), pool, nodes)
+	forPool := c.speakersForPool(pool, nodes)
 	var availableNodes []string
 	if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
 		availableNodes = usableNodes(eps, forPool)
@@ -205,14 +206,14 @@ func poolMatchesNodeL2(pool *config.Pool, node string) bool {
 	return false
 }
 
-func speakersForPool(speakers map[string]bool, pool *config.Pool, nodes map[string]*v1.Node) map[string]bool {
+func (c *layer2Controller) speakersForPool(pool *config.Pool, nodes map[string]*v1.Node) map[string]bool {
 	res := map[string]bool{}
-	for s := range speakers {
+	for s := range c.sList.UsableSpeakers() {
 		if k8snodes.IsNetworkUnavailable(nodes[s]) {
 			continue
 		}
 
-		if k8snodes.IsNodeExcludedFromBalancers(nodes[s]) {
+		if !c.ignoreExcludeLB && k8snodes.IsNodeExcludedFromBalancers(nodes[s]) {
 			continue
 		}
 
diff --git a/speaker/main.go b/speaker/main.go
index e10269a2..fd81f024 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -82,6 +82,7 @@ func main() {
 		logLevel          = flag.String("log-level", "info", fmt.Sprintf("log level. must be one of: [%s]", logging.Levels.String()))
 		enablePprof       = flag.Bool("enable-pprof", false, "Enable pprof profiling")
 		loadBalancerClass = flag.String("lb-class", "", "load balancer class. When enabled, metallb will handle only services whose spec.loadBalancerClass matches the given lb class")
+		ignoreLBExclude   = flag.Bool("ignore-exclude-lb", false, "ignore the exclude-from-external-load-balancers label")
 	)
 	flag.Parse()
 
@@ -157,6 +158,7 @@ func main() {
 		SList:                  sList,
 		bgpType:                bgpImplementation(bgpType),
 		InterfaceExcludeRegexp: interfacesToExclude,
+		IgnoreExcludeLB:        *ignoreLBExclude,
 	})
 	if err != nil {
 		level.Error(logger).Log("op", "startup", "error", err, "msg", "failed to create MetalLB controller")
@@ -240,16 +242,18 @@ type controllerConfig struct {
 	SupportedProtocols           []config.Proto
 	AnnouncedInterfacesToExclude []string `yaml:"announcedInterfacesToExclude"`
 	InterfaceExcludeRegexp       *regexp.Regexp
+	IgnoreExcludeLB              bool
 }
 
 func newController(cfg controllerConfig) (*controller, error) {
 	handlers := map[config.Proto]Protocol{
 		config.BGP: &bgpController{
-			logger:         cfg.Logger,
-			myNode:         cfg.MyNode,
-			svcAds:         make(map[string][]*bgp.Advertisement),
-			bgpType:        cfg.bgpType,
-			sessionManager: newBGP(cfg),
+			logger:          cfg.Logger,
+			myNode:          cfg.MyNode,
+			svcAds:          make(map[string][]*bgp.Advertisement),
+			bgpType:         cfg.bgpType,
+			sessionManager:  newBGP(cfg),
+			ignoreExcludeLB: cfg.IgnoreExcludeLB,
 		},
 	}
 	protocols := []config.Proto{config.BGP}
@@ -260,9 +264,10 @@ func newController(cfg controllerConfig) (*controller, error) {
 			return nil, fmt.Errorf("making layer2 announcer: %s", err)
 		}
 		handlers[config.Layer2] = &layer2Controller{
-			announcer: a,
-			myNode:    cfg.MyNode,
-			sList:     cfg.SList,
+			announcer:       a,
+			myNode:          cfg.MyNode,
+			sList:           cfg.SList,
+			ignoreExcludeLB: cfg.IgnoreExcludeLB,
 		}
 		protocols = append(protocols, config.Layer2)
 	}
diff --git a/website/content/release-notes/_index.md b/website/content/release-notes/_index.md
index a4707c81..def2fe17 100644
--- a/website/content/release-notes/_index.md
+++ b/website/content/release-notes/_index.md
@@ -9,6 +9,9 @@ New features:
 
 - Bump FRR to 9.0.2 ([PR 2282](https://github.com/metallb/metallb/pull/2282), [Issue 2256](https://github.com/metallb/metallb/issues/2256))
 - Make the namse of webhook service and cert metallb specific to avoid name conflicts with other resources in the cluster. ([PR 2244](https://github.com/metallb/metallb/pull/2244), [Issue 2174](https://github.com/metallb/metallb/issues/2174)).
+New Features:
+
+- Provide a "ignore-exclude-lb" flag to ignore the `exclude-from-external-load-balancers` label ([PR 2280](https://github.com/metallb/metallb/pull/2280), [Issue 2274](https://github.com/metallb/metallb/issues/2274))
 
 BugFixes:
 
@@ -16,6 +19,7 @@ BugFixes:
 - Remove dangling AddressPool leftovers ([PR 2272](https://github.com/metallb/metallb/pull/2272) [Issue 2270](https://github.com/metallb/metallb/issues/2270))
 - Helm: fix the creation of the metrics-certs volume under the presence of the speakerMetricsTLSSecret value, regardless of FRR being enabled ([PR 2286](https://github.com/metallb/metallb/pull/2286))
 - Docs: remove outdated information about multiprotocol services ([PR 2228](https://github.com/metallb/metallb/pull/2228)).
+- FRR Config: generate the route-map for each local pref / community / large community only once ([PR 2292](https://github.com/metallb/metallb/pull/2292))
 
 Chores:
 
diff --git a/website/content/troubleshooting/_index.md b/website/content/troubleshooting/_index.md
index db9b2018..9f37781f 100644
--- a/website/content/troubleshooting/_index.md
+++ b/website/content/troubleshooting/_index.md
@@ -87,6 +87,13 @@ A given speaker won't advertise the service if:
 - there are no L2Advertisements / BGPAdvertisements matching the speaker node (if node selectors are specified)
 - the Kubernetes API reports "network not available" on the speaker's node
 
+## MetalLB is not advertising my service from my control-plane nodes or from my single node cluster
+
+Make sure your nodes are not labeled with the
+[node.kubernetes.io/exclude-from-external-load-balancers](https://kubernetes.io/docs/reference/labels-annotations-taints/#node-kubernetes-io-exclude-from-external-load-balancers) label.
+MetalLB honors that label and won't announce any service from such nodes. One way to circumvent
+the issue is to provide the speakers with the `--ignore-exclude-lb` flag (either from Helm or via Kustomize).
+
 ## MetalLB says it advertises the service but reaching the service does not work
 
 ### Checking the L2 advertisement works


Test patch:
diff --git a/internal/bgp/frr/frr_test.go b/internal/bgp/frr/frr_test.go
index 3c5c38a5..097e939a 100644
--- a/internal/bgp/frr/frr_test.go
+++ b/internal/bgp/frr/frr_test.go
@@ -1014,6 +1014,54 @@ func TestLargeCommunities(t *testing.T) {
 	testCheckConfigFile(t)
 }
 
+func TestManyAdvertisementsSameCommunity(t *testing.T) {
+	testSetup(t)
+
+	l := log.NewNopLogger()
+	sessionManager := mockNewSessionManager(l, logging.LevelInfo)
+	defer close(sessionManager.reloadConfig)
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerAddress:   "10.2.2.254:179",
+			SourceAddress: net.ParseIP("10.1.1.254"),
+			MyASN:         100,
+			RouterID:      net.ParseIP("10.1.1.254"),
+			PeerASN:       200,
+			HoldTime:      time.Second,
+			KeepAliveTime: time.Second,
+			ConnectTime:   ptr.To(time.Second),
+			Password:      "password",
+			CurrentNode:   "hostname",
+			EBGPMultiHop:  true,
+			SessionName:   "test-peer"})
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	community1, _ := community.New("1111:2222")
+	communities := []community.BGPCommunity{community1}
+	advs := []*bgp.Advertisement{}
+	for i := 0; i < 10; i++ {
+		prefix := &net.IPNet{
+			IP:   net.ParseIP(fmt.Sprintf("172.16.1.%d", i)),
+			Mask: classCMask,
+		}
+		adv := &bgp.Advertisement{
+			Prefix:      prefix,
+			Communities: communities,
+		}
+		advs = append(advs, adv)
+	}
+
+	err = session.Set(advs...)
+	if err != nil {
+		t.Fatalf("Could not advertise prefix: %s", err)
+	}
+
+	testCheckConfigFile(t)
+}
+
 func TestAddToAdvertisements(t *testing.T) {
 	tests := []struct {
 		name      string
diff --git a/internal/bgp/frr/testdata/TestLargeCommunities.golden b/internal/bgp/frr/testdata/TestLargeCommunities.golden
index 4603c28a..e145ba4c 100644
--- a/internal/bgp/frr/testdata/TestLargeCommunities.golden
+++ b/internal/bgp/frr/testdata/TestLargeCommunities.golden
@@ -4,32 +4,30 @@ hostname dummyhostname
 ip nht resolve-via-default
 ipv6 nht resolve-via-default
 route-map 10.2.2.254-in deny 20
-
-
-ip prefix-list 10.2.2.254-300-ipv4-localpref-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-out permit 1
   match ip address prefix-list 10.2.2.254-300-ipv4-localpref-prefixes
   set local-preference 300
   on-match next
-
-ip prefix-list 10.2.2.254-3333:4444-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-out permit 2
-  match ip address prefix-list 10.2.2.254-3333:4444-ipv4-community-prefixes
-  set community 3333:4444 additive
-  on-match next
-ip prefix-list 10.2.2.254-large:1111:2222:3333-ipv4-community-prefixes permit 172.16.1.10/24
-route-map 10.2.2.254-out permit 3
   match ip address prefix-list 10.2.2.254-large:1111:2222:3333-ipv4-community-prefixes
   set large-community 1111:2222:3333 additive
   on-match next
-ip prefix-list 10.2.2.254-large:2222:3333:4444-ipv4-community-prefixes permit 172.16.1.10/24
-route-map 10.2.2.254-out permit 4
+route-map 10.2.2.254-out permit 3
   match ip address prefix-list 10.2.2.254-large:2222:3333:4444-ipv4-community-prefixes
   set large-community 2222:3333:4444 additive
   on-match next
+route-map 10.2.2.254-out permit 4
+  match ip address prefix-list 10.2.2.254-3333:4444-ipv4-community-prefixes
+  set community 3333:4444 additive
+  on-match next
+
+ip prefix-list 10.2.2.254-300-ipv4-localpref-prefixes seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-3333:4444-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-large:1111:2222:3333-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-large:2222:3333:4444-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 
 
diff --git a/internal/bgp/frr/testdata/TestManyAdvertisementsSameCommunity.golden b/internal/bgp/frr/testdata/TestManyAdvertisementsSameCommunity.golden
new file mode 100644
index 00000000..93a8f5be
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestManyAdvertisementsSameCommunity.golden
@@ -0,0 +1,109 @@
+log file /etc/frr/frr.log 
+log timestamp precision 3
+hostname dummyhostname
+ip nht resolve-via-default
+ipv6 nht resolve-via-default
+route-map 10.2.2.254-in deny 20
+route-map 10.2.2.254-out permit 1
+  match ip address prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes
+  set community 1111:2222 additive
+  on-match next
+
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.0/24
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.0/24
+
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 2 permit 172.16.1.1/24
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 2 permit 172.16.1.1/24
+
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 3 permit 172.16.1.2/24
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 3 permit 172.16.1.2/24
+
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 4 permit 172.16.1.3/24
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 4 permit 172.16.1.3/24
+
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 5 permit 172.16.1.4/24
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 5 permit 172.16.1.4/24
+
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 6 permit 172.16.1.5/24
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 6 permit 172.16.1.5/24
+
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 7 permit 172.16.1.6/24
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 7 permit 172.16.1.6/24
+
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 8 permit 172.16.1.7/24
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 8 permit 172.16.1.7/24
+
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 9 permit 172.16.1.8/24
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 9 permit 172.16.1.8/24
+
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 10 permit 172.16.1.9/24
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 10 permit 172.16.1.9/24
+
+
+
+
+ipv6 prefix-list 10.2.2.254-pl-ipv4 seq 11 deny any
+
+route-map 10.2.2.254-out permit 2
+  match ip address prefix-list 10.2.2.254-pl-ipv4
+route-map 10.2.2.254-out permit 3
+  match ipv6 address prefix-list 10.2.2.254-pl-ipv4
+
+router bgp 100
+  no bgp ebgp-requires-policy
+  no bgp network import-check
+  no bgp default ipv4-unicast
+
+  bgp router-id 10.1.1.254
+  neighbor 10.2.2.254 remote-as 200
+  neighbor 10.2.2.254 ebgp-multihop
+  neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 1 1
+  neighbor 10.2.2.254 timers connect 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
+
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+    neighbor 10.2.2.254 route-map 10.2.2.254-in in
+    neighbor 10.2.2.254 route-map 10.2.2.254-out out
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+    neighbor 10.2.2.254 route-map 10.2.2.254-in in
+    neighbor 10.2.2.254 route-map 10.2.2.254-out out
+  exit-address-family
+  address-family ipv4 unicast
+    network 172.16.1.0/24
+    network 172.16.1.1/24
+    network 172.16.1.2/24
+    network 172.16.1.3/24
+    network 172.16.1.4/24
+    network 172.16.1.5/24
+    network 172.16.1.6/24
+    network 172.16.1.7/24
+    network 172.16.1.8/24
+    network 172.16.1.9/24
+  exit-address-family
+
+
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisement.golden b/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
index 9d618377..a4142cdb 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
@@ -4,28 +4,25 @@ hostname dummyhostname
 ip nht resolve-via-default
 ipv6 nht resolve-via-default
 route-map 10.2.2.254-in deny 20
-
-
-ip prefix-list 10.2.2.254-300-ipv4-localpref-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-out permit 1
   match ip address prefix-list 10.2.2.254-300-ipv4-localpref-prefixes
   set local-preference 300
   on-match next
-
-ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-out permit 2
   match ip address prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes
   set community 1111:2222 additive
   on-match next
-
-ip prefix-list 10.2.2.254-3333:4444-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-out permit 3
   match ip address prefix-list 10.2.2.254-3333:4444-ipv4-community-prefixes
   set community 3333:4444 additive
   on-match next
 
+ip prefix-list 10.2.2.254-300-ipv4-localpref-prefixes seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-3333:4444-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
+
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 
 
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
index 13aeed6b..f1559c98 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
@@ -7,7 +7,7 @@ route-map 10.2.2.254-in deny 20
 
 
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.11/24
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.11/24
 
 
 
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementChangeVRF.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementChangeVRF.golden
index d566b087..b8d3f87d 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementChangeVRF.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementChangeVRF.golden
@@ -7,7 +7,7 @@ route-map 10.2.2.254-red-in deny 20
 
 
 
- ip prefix-list 10.2.2.254-red-pl-ipv4 seq 1 permit 172.16.1.11/24
+ip prefix-list 10.2.2.254-red-pl-ipv4 seq 1 permit 172.16.1.11/24
 
 
 
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
index 935ccec6..f73ff571 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
@@ -7,7 +7,7 @@ route-map 10.2.2.254-in deny 20
 
 
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 
 
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementVRF.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementVRF.golden
index 9a1f7e2c..a0872797 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementVRF.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementVRF.golden
@@ -4,28 +4,25 @@ hostname dummyhostname
 ip nht resolve-via-default
 ipv6 nht resolve-via-default
 route-map 10.2.2.254-red-in deny 20
-
-
-ip prefix-list 10.2.2.254-red-300-ipv4-localpref-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-red-out permit 1
   match ip address prefix-list 10.2.2.254-red-300-ipv4-localpref-prefixes
   set local-preference 300
   on-match next
-
-ip prefix-list 10.2.2.254-red-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-red-out permit 2
   match ip address prefix-list 10.2.2.254-red-1111:2222-ipv4-community-prefixes
   set community 1111:2222 additive
   on-match next
-
-ip prefix-list 10.2.2.254-red-3333:4444-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-red-out permit 3
   match ip address prefix-list 10.2.2.254-red-3333:4444-ipv4-community-prefixes
   set community 3333:4444 additive
   on-match next
 
+ip prefix-list 10.2.2.254-red-300-ipv4-localpref-prefixes seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-red-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-red-3333:4444-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
+
 
- ip prefix-list 10.2.2.254-red-pl-ipv4 seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-red-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 
 
diff --git a/internal/bgp/frr/testdata/TestTwoAdvertisements.golden b/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
index 201b976f..63e6baba 100644
--- a/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
+++ b/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
@@ -4,20 +4,19 @@ hostname dummyhostname
 ip nht resolve-via-default
 ipv6 nht resolve-via-default
 route-map 10.2.2.254-in deny 20
-
-
-ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-out permit 1
   match ip address prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes
   set community 1111:2222 additive
   on-match next
 
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
+
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 2 permit 172.16.1.11/24
+ip prefix-list 10.2.2.254-pl-ipv4 seq 2 permit 172.16.1.11/24
 
 
 
diff --git a/internal/bgp/frr/testdata/TestTwoAdvertisementsDuplicate.golden b/internal/bgp/frr/testdata/TestTwoAdvertisementsDuplicate.golden
index 0e34c39b..cc17ec86 100644
--- a/internal/bgp/frr/testdata/TestTwoAdvertisementsDuplicate.golden
+++ b/internal/bgp/frr/testdata/TestTwoAdvertisementsDuplicate.golden
@@ -7,7 +7,7 @@ route-map 10.2.2.254-in deny 20
 
 
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 
 
diff --git a/internal/bgp/frr/testdata/TestTwoAdvertisementsTwoSessions.golden b/internal/bgp/frr/testdata/TestTwoAdvertisementsTwoSessions.golden
index affe5450..d18ef7f6 100644
--- a/internal/bgp/frr/testdata/TestTwoAdvertisementsTwoSessions.golden
+++ b/internal/bgp/frr/testdata/TestTwoAdvertisementsTwoSessions.golden
@@ -4,78 +4,64 @@ hostname dummyhostname
 ip nht resolve-via-default
 ipv6 nht resolve-via-default
 route-map 10.2.2.254-in deny 20
-
-
-ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-out permit 1
+  match ip address prefix-list 10.2.2.254-2-ipv4-localpref-prefixes
+  set local-preference 2
+  on-match next
+route-map 10.2.2.254-out permit 2
   match ip address prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes
   set community 1111:2222 additive
   on-match next
 
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
 
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 ip prefix-list 10.2.2.254-2-ipv4-localpref-prefixes seq 1 permit 172.16.1.11/24
-route-map 10.2.2.254-out permit 2
-  match ip address prefix-list 10.2.2.254-2-ipv4-localpref-prefixes
-  set local-preference 2
-  on-match next
-
 ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 2 permit 172.16.1.11/24
-route-map 10.2.2.254-out permit 3
-  match ip address prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes
-  set community 1111:2222 additive
-  on-match next
 
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 2 permit 172.16.1.11/24
+ip prefix-list 10.2.2.254-pl-ipv4 seq 2 permit 172.16.1.11/24
 
 
 
 
 ipv6 prefix-list 10.2.2.254-pl-ipv4 seq 3 deny any
 
-route-map 10.2.2.254-out permit 4
+route-map 10.2.2.254-out permit 3
   match ip address prefix-list 10.2.2.254-pl-ipv4
-route-map 10.2.2.254-out permit 5
+route-map 10.2.2.254-out permit 4
   match ipv6 address prefix-list 10.2.2.254-pl-ipv4
 route-map 10.2.2.255-in deny 20
-
-
-ip prefix-list 10.2.2.255-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.255-out permit 1
+  match ip address prefix-list 10.2.2.255-2-ipv4-localpref-prefixes
+  set local-preference 2
+  on-match next
+route-map 10.2.2.255-out permit 2
   match ip address prefix-list 10.2.2.255-1111:2222-ipv4-community-prefixes
   set community 1111:2222 additive
   on-match next
 
+ip prefix-list 10.2.2.255-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 
- ip prefix-list 10.2.2.255-pl-ipv4 seq 1 permit 172.16.1.10/24
 
+ip prefix-list 10.2.2.255-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 ip prefix-list 10.2.2.255-2-ipv4-localpref-prefixes seq 1 permit 172.16.1.11/24
-route-map 10.2.2.255-out permit 2
-  match ip address prefix-list 10.2.2.255-2-ipv4-localpref-prefixes
-  set local-preference 2
-  on-match next
-
 ip prefix-list 10.2.2.255-1111:2222-ipv4-community-prefixes seq 2 permit 172.16.1.11/24
-route-map 10.2.2.255-out permit 3
-  match ip address prefix-list 10.2.2.255-1111:2222-ipv4-community-prefixes
-  set community 1111:2222 additive
-  on-match next
 
 
- ip prefix-list 10.2.2.255-pl-ipv4 seq 2 permit 172.16.1.11/24
+ip prefix-list 10.2.2.255-pl-ipv4 seq 2 permit 172.16.1.11/24
 
 
 
 
 ipv6 prefix-list 10.2.2.255-pl-ipv4 seq 3 deny any
 
-route-map 10.2.2.255-out permit 4
+route-map 10.2.2.255-out permit 3
   match ip address prefix-list 10.2.2.255-pl-ipv4
-route-map 10.2.2.255-out permit 5
+route-map 10.2.2.255-out permit 4
   match ipv6 address prefix-list 10.2.2.255-pl-ipv4
 
 router bgp 100
diff --git a/internal/bgp/frr/testdata/TestTwoAdvertisementsTwoSessionsOneVRF.golden b/internal/bgp/frr/testdata/TestTwoAdvertisementsTwoSessionsOneVRF.golden
index ae06504a..f1d6d931 100644
--- a/internal/bgp/frr/testdata/TestTwoAdvertisementsTwoSessionsOneVRF.golden
+++ b/internal/bgp/frr/testdata/TestTwoAdvertisementsTwoSessionsOneVRF.golden
@@ -4,78 +4,64 @@ hostname dummyhostname
 ip nht resolve-via-default
 ipv6 nht resolve-via-default
 route-map 10.2.2.254-in deny 20
-
-
-ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-out permit 1
+  match ip address prefix-list 10.2.2.254-2-ipv4-localpref-prefixes
+  set local-preference 2
+  on-match next
+route-map 10.2.2.254-out permit 2
   match ip address prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes
   set community 1111:2222 additive
   on-match next
 
+ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
 
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 ip prefix-list 10.2.2.254-2-ipv4-localpref-prefixes seq 1 permit 172.16.1.11/24
-route-map 10.2.2.254-out permit 2
-  match ip address prefix-list 10.2.2.254-2-ipv4-localpref-prefixes
-  set local-preference 2
-  on-match next
-
 ip prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes seq 2 permit 172.16.1.11/24
-route-map 10.2.2.254-out permit 3
-  match ip address prefix-list 10.2.2.254-1111:2222-ipv4-community-prefixes
-  set community 1111:2222 additive
-  on-match next
 
 
- ip prefix-list 10.2.2.254-pl-ipv4 seq 2 permit 172.16.1.11/24
+ip prefix-list 10.2.2.254-pl-ipv4 seq 2 permit 172.16.1.11/24
 
 
 
 
 ipv6 prefix-list 10.2.2.254-pl-ipv4 seq 3 deny any
 
-route-map 10.2.2.254-out permit 4
+route-map 10.2.2.254-out permit 3
   match ip address prefix-list 10.2.2.254-pl-ipv4
-route-map 10.2.2.254-out permit 5
+route-map 10.2.2.254-out permit 4
   match ipv6 address prefix-list 10.2.2.254-pl-ipv4
 route-map 10.2.2.255-red-in deny 20
-
-
-ip prefix-list 10.2.2.255-red-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.255-red-out permit 1
+  match ip address prefix-list 10.2.2.255-red-2-ipv4-localpref-prefixes
+  set local-preference 2
+  on-match next
+route-map 10.2.2.255-red-out permit 2
   match ip address prefix-list 10.2.2.255-red-1111:2222-ipv4-community-prefixes
   set community 1111:2222 additive
   on-match next
 
+ip prefix-list 10.2.2.255-red-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 
- ip prefix-list 10.2.2.255-red-pl-ipv4 seq 1 permit 172.16.1.10/24
 
+ip prefix-list 10.2.2.255-red-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 ip prefix-list 10.2.2.255-red-2-ipv4-localpref-prefixes seq 1 permit 172.16.1.11/24
-route-map 10.2.2.255-red-out permit 2
-  match ip address prefix-list 10.2.2.255-red-2-ipv4-localpref-prefixes
-  set local-preference 2
-  on-match next
-
 ip prefix-list 10.2.2.255-red-1111:2222-ipv4-community-prefixes seq 2 permit 172.16.1.11/24
-route-map 10.2.2.255-red-out permit 3
-  match ip address prefix-list 10.2.2.255-red-1111:2222-ipv4-community-prefixes
-  set community 1111:2222 additive
-  on-match next
 
 
- ip prefix-list 10.2.2.255-red-pl-ipv4 seq 2 permit 172.16.1.11/24
+ip prefix-list 10.2.2.255-red-pl-ipv4 seq 2 permit 172.16.1.11/24
 
 
 
 
 ipv6 prefix-list 10.2.2.255-red-pl-ipv4 seq 3 deny any
 
-route-map 10.2.2.255-red-out permit 4
+route-map 10.2.2.255-red-out permit 3
   match ip address prefix-list 10.2.2.255-red-pl-ipv4
-route-map 10.2.2.255-red-out permit 5
+route-map 10.2.2.255-red-out permit 4
   match ipv6 address prefix-list 10.2.2.255-red-pl-ipv4
 
 router bgp 100
diff --git a/internal/bgp/frr/testdata/TestTwoAdvertisementsVRF.golden b/internal/bgp/frr/testdata/TestTwoAdvertisementsVRF.golden
index 5ebe0fed..323e0932 100644
--- a/internal/bgp/frr/testdata/TestTwoAdvertisementsVRF.golden
+++ b/internal/bgp/frr/testdata/TestTwoAdvertisementsVRF.golden
@@ -4,20 +4,19 @@ hostname dummyhostname
 ip nht resolve-via-default
 ipv6 nht resolve-via-default
 route-map 10.2.2.254-red-in deny 20
-
-
-ip prefix-list 10.2.2.254-red-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
 route-map 10.2.2.254-red-out permit 1
   match ip address prefix-list 10.2.2.254-red-1111:2222-ipv4-community-prefixes
   set community 1111:2222 additive
   on-match next
 
+ip prefix-list 10.2.2.254-red-1111:2222-ipv4-community-prefixes seq 1 permit 172.16.1.10/24
+
 
- ip prefix-list 10.2.2.254-red-pl-ipv4 seq 1 permit 172.16.1.10/24
+ip prefix-list 10.2.2.254-red-pl-ipv4 seq 1 permit 172.16.1.10/24
 
 
 
- ip prefix-list 10.2.2.254-red-pl-ipv4 seq 2 permit 172.16.1.11/24
+ip prefix-list 10.2.2.254-red-pl-ipv4 seq 2 permit 172.16.1.11/24
 
 
 
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index db310c62..725b385a 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"os"
 	"sort"
 	"sync"
 	"testing"
@@ -1340,3 +1341,146 @@ func TestNodeSelectors(t *testing.T) {
 		}
 	}
 }
+
+func TestShouldAnnounceExcludeLB(t *testing.T) {
+	epsOn := func(node string) map[string][]discovery.EndpointSlice {
+		return map[string][]discovery.EndpointSlice{
+			"10.20.30.1": {
+				{
+					Endpoints: []discovery.Endpoint{
+						{
+							Addresses: []string{
+								"2.3.4.5",
+							},
+							NodeName: ptr.To(node),
+							Conditions: discovery.EndpointConditions{
+								Ready: ptr.To(true),
+							},
+						},
+					},
+				},
+			},
+		}
+	}
+
+	tests := []struct {
+		desc string
+
+		balancer            string
+		eps                 map[string][]discovery.EndpointSlice
+		trafficPolicy       v1.ServiceExternalTrafficPolicyType
+		excludeFromLB       []string
+		ignoreExcludeFromLB bool
+		c1ExpectedResult    map[string]string
+		c2ExpectedResult    map[string]string
+	}{
+		{
+			desc:          "One service, endpoint on iris1, no selector, both excluded, both should not announce",
+			balancer:      "test1",
+			eps:           epsOn("iris1"),
+			trafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
+			excludeFromLB: []string{"iris1", "iris2"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "nodeLabeledExcludeBalancers",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "nodeLabeledExcludeBalancers",
+			},
+		},
+		{
+			desc:          "One service, endpoint on iris1, no selector, etplocal, ignore excludelb, both should announce",
+			balancer:      "test1",
+			eps:           epsOn("iris1"),
+			trafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
+			excludeFromLB: []string{"iris1", "iris2"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			ignoreExcludeFromLB: true,
+		},
+	}
+	l := log.NewNopLogger()
+	for _, test := range tests {
+		cfg := config.Config{
+			Pools: &config.Pools{ByName: map[string]*config.Pool{
+				"default": {
+					CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+					BGPAdvertisements: []*config.BGPAdvertisement{{
+						Nodes: map[string]bool{
+							"iris1": true,
+							"iris2": true,
+						},
+					}},
+				},
+			}},
+		}
+		c1, err := newController(controllerConfig{
+			MyNode:          "iris1",
+			Logger:          log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+			bgpType:         bgpNative,
+			IgnoreExcludeLB: test.ignoreExcludeFromLB,
+		})
+		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)),
+			bgpType:         bgpNative,
+			IgnoreExcludeLB: test.ignoreExcludeFromLB,
+		})
+		if err != nil {
+			t.Fatalf("creating controller: %s", err)
+		}
+		c2.client = &testK8S{t: t}
+
+		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)
+		lbIPStr := lbIP.String()
+
+		nodes := map[string]*v1.Node{
+			"iris1": {
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "iris1",
+				},
+			},
+			"iris2": {
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "iris2",
+				},
+			},
+		}
+		for _, n := range test.excludeFromLB {
+			nodes[n].Labels = map[string]string{
+				v1.LabelNodeExcludeBalancers: "",
+			}
+		}
+
+		response1 := c1.protocolHandlers[config.BGP].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nodes)
+		response2 := c2.protocolHandlers[config.BGP].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nodes)
+		if response1 != test.c1ExpectedResult[lbIPStr] {
+			t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIPStr, test.c1ExpectedResult[lbIPStr], response1)
+		}
+		if response2 != test.c2ExpectedResult[lbIPStr] {
+			t.Errorf("%q: shouldAnnounce for controller 2 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIPStr, test.c2ExpectedResult[lbIPStr], response2)
+		}
+	}
+}
diff --git a/speaker/layer2_controller_test.go b/speaker/layer2_controller_test.go
index 9f42da54..ad640ce6 100644
--- a/speaker/layer2_controller_test.go
+++ b/speaker/layer2_controller_test.go
@@ -7,7 +7,6 @@ import (
 	"net"
 	"os"
 	"sort"
-	"strings"
 	"testing"
 
 	"go.universe.tf/metallb/internal/config"
@@ -1122,34 +1121,13 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 	}
 }
 
-func TestShouldAnnounceNodeSelector(t *testing.T) {
+func TestShouldAnnounceFromNodes(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{
@@ -1238,12 +1216,14 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 	tests := []struct {
 		desc string
 
-		balancer         string
-		L2Advertisements []*config.L2Advertisement
-		eps              map[string][]discovery.EndpointSlice
-		trafficPolicy    v1.ServiceExternalTrafficPolicyType
-		c1ExpectedResult map[string]string
-		c2ExpectedResult map[string]string
+		balancer            string
+		L2Advertisements    []*config.L2Advertisement
+		eps                 map[string][]discovery.EndpointSlice
+		trafficPolicy       v1.ServiceExternalTrafficPolicyType
+		excludeFromLB       []string
+		ignoreExcludeFromLB bool
+		c1ExpectedResult    map[string]string
+		c2ExpectedResult    map[string]string
 	}{
 		{
 			desc:             "One service, endpoint on iris1, selector on iris1, c1 should announce",
@@ -1261,7 +1241,7 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 		{
 			desc:             "One service, etp local, endpoint on iris1, selector on both(split), c2 should announce",
 			balancer:         "test1",
-			eps:              epsOn("iris2"),
+			eps:              epsOn("iris1"),
 			L2Advertisements: advertisementSplit,
 			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeLocal,
 			c1ExpectedResult: map[string]string{
@@ -1310,13 +1290,66 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 				"10.20.30.1": "notOwner",
 			},
 		},
+		{
+			desc:             "One service, endpoint on iris1, no selector, iris2 excluded, c1 should announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementsForBoth,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeCluster,
+			excludeFromLB:    []string{"iris2"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+		},
+		{
+			desc:             "One service, endpoint on iris1, no selector, iris1 excluded, c2 should announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementsForBoth,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeCluster,
+			excludeFromLB:    []string{"iris1"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+		},
+		{
+			desc:             "One service, endpoint on iris1, no selector, both excluded, both should not announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementsForBoth,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeCluster,
+			excludeFromLB:    []string{"iris1", "iris2"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+		},
+		{
+			desc:             "One service, endpoint on iris1, no selector, etplocal, both excluded, ignore excludelb, c1 should announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementsForBoth,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeLocal,
+			excludeFromLB:    []string{"iris1", "iris2"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+			ignoreExcludeFromLB: true,
+		},
 	}
 	l := log.NewNopLogger()
 	for _, test := range tests {
-		if !strings.Contains(test.desc, "ltp") {
-			continue
-		}
-
 		cfg := config.Config{
 			Pools: &config.Pools{ByName: map[string]*config.Pool{
 				"default": {
@@ -1325,6 +1358,30 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 				},
 			}},
 		}
+		c1, err := newController(controllerConfig{
+			MyNode:          "iris1",
+			Logger:          log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+			SList:           fakeSL,
+			bgpType:         bgpNative,
+			IgnoreExcludeLB: test.ignoreExcludeFromLB,
+		})
+		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,
+			IgnoreExcludeLB: test.ignoreExcludeFromLB,
+		})
+		if err != nil {
+			t.Fatalf("creating controller: %s", err)
+		}
+		c2.client = &testK8S{t: t}
+
 		if c1.SetConfig(l, &cfg) == controllers.SyncStateError {
 			t.Errorf("%q: SetConfig failed", test.desc)
 		}
@@ -1341,8 +1398,27 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 
 		lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
 		lbIPStr := lbIP.String()
-		response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nil)
-		response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nil)
+
+		nodes := map[string]*v1.Node{
+			"iris1": {
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "iris1",
+				},
+			},
+			"iris2": {
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "iris2",
+				},
+			},
+		}
+		for _, n := range test.excludeFromLB {
+			nodes[n].Labels = map[string]string{
+				v1.LabelNodeExcludeBalancers: "",
+			}
+		}
+
+		response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nodes)
+		response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nodes)
 		if response1 != test.c1ExpectedResult[lbIPStr] {
 			t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIPStr, test.c1ExpectedResult[lbIPStr], response1)
 		}