Commit 981474b85b for frr

commit 981474b85b0b214d06b1435ec6d381027c995762
Author: Robin Christ <r.christ@partimus.com>
Date:   Wed Oct 14 10:45:16 2026 +0200

    bgpd: let the EVPN code own the VRF RT list rebuild

    34afeb5b10 made EVPN VRF RT changes batched for efficiency reasons.
    It added new functions like bgp_evpn_vrf_rt_change_begin()/end()
    around new _deferred setters, which are called by the VTY / CLI.
    That means a bug in the CLI could leave EVPN in an inconsistent state.

    This commit adds new _batch functions to EVPN core code, moving
    the responsibility for consistency back to the EVPN core code.
    The old single-RT setters have had no caller since 34afeb5b10
    and are dropped as well.

    Signed-off-by: Robin Christ <r.christ@partimus.com>

diff --git a/bgpd/bgp_evpn.c b/bgpd/bgp_evpn.c
index 86c7d2f18e..138b01bb8f 100644
--- a/bgpd/bgp_evpn.c
+++ b/bgpd/bgp_evpn.c
@@ -6607,24 +6607,6 @@ static void evpn_vrf_rt_routes_unmap(struct bgp *bgp_vrf)
 	bgp_evpn_unmap_vrf_from_its_rts(bgp_vrf);
 }

-void bgp_evpn_vrf_rt_change_begin(struct bgp *bgp_vrf, bool is_import)
-{
-	if (is_import)
-		evpn_vrf_rt_routes_unmap(bgp_vrf);
-}
-
-void bgp_evpn_vrf_rt_change_end(struct bgp *bgp_vrf, bool is_import)
-{
-	if (is_import) {
-		bgp_evpn_vrf_regenerate_effective_import_rts(bgp_vrf);
-		evpn_vrf_rt_routes_map(bgp_vrf);
-	} else {
-		bgp_evpn_vrf_regenerate_effective_export_rts(bgp_vrf);
-		if (is_l3vni_live(bgp_vrf))
-			bgp_evpn_handle_export_rt_change_for_vrf(bgp_vrf);
-	}
-}
-
 /* Remove a route target from a configured route target list and free it */
 static void cfgd_rt_list_del(struct bgp_evpn_cfgd_rt_slu_head *head,
 			     struct bgp_evpn_cfgd_rt *cfgd_rt)
@@ -6643,29 +6625,54 @@ static void cfgd_rt_list_add(struct bgp_evpn_cfgd_rt_slu_head *head,
 		bgp_evpn_cfgd_rt_free(cfgd_rt);
 }

-/*
- * Add one import RT to the VRF without touching the RT-to-VRF mapping or
- * the imported routes. The caller owns that rebuild: it must have called
- * bgp_evpn_vrf_rt_change_begin() before the first RT of the list and must
- * call bgp_evpn_vrf_rt_change_end() after the last one, otherwise the VRF
- * is left with no effective imports.
- *
- * This split exists so a whole RTLIST walks the EVPN table once (one unmap
- * and one map) instead of twice per RT.
+/* Move every RT of cfgd_rts into a configured route target list, leaving
+ * cfgd_rts empty.
  */
-void bgp_evpn_configure_import_rt_for_vrf_deferred(struct bgp *bgp_vrf,
-						   struct bgp_evpn_cfgd_rt *cfgd_rt)
+static void cfgd_rt_list_add_batch(struct bgp_evpn_cfgd_rt_slu_head *head,
+				   struct bgp_evpn_cfgd_rt_slu_head *cfgd_rts)
 {
-	struct bgp_evpn_rt_config *rt_config = bgp_vrf->vrf_route_target_config;
+	struct bgp_evpn_cfgd_rt *cfgd_rt;
+
+	while ((cfgd_rt = bgp_evpn_cfgd_rt_slu_pop(cfgd_rts)))
+		cfgd_rt_list_add(head, cfgd_rt);
+}
+
+/* Remove every RT of cfgd_rts from a configured route target list, then
+ * free cfgd_rts' entries, leaving it empty. RTs not configured are ignored.
+ */
+static void cfgd_rt_list_del_batch(struct bgp_evpn_cfgd_rt_slu_head *head,
+				   struct bgp_evpn_cfgd_rt_slu_head *cfgd_rts)
+{
+	struct bgp_evpn_cfgd_rt *cfgd_rt, *existing;

-	cfgd_rt_list_add(&rt_config->cfgd_import, cfgd_rt);
+	while ((cfgd_rt = bgp_evpn_cfgd_rt_slu_pop(cfgd_rts))) {
+		existing = bgp_evpn_cfgd_rt_slu_find(head, cfgd_rt);
+		if (existing)
+			cfgd_rt_list_del(head, existing);
+		bgp_evpn_cfgd_rt_free(cfgd_rt);
+	}
 }

-void bgp_evpn_configure_import_rt_for_vrf(struct bgp *bgp_vrf, struct bgp_evpn_cfgd_rt *cfgd_rt)
+/* Add a set of import RTs to the VRF, taking ownership of every entry and
+ * leaving cfgd_rts empty. The VRF's routes are unmapped and remapped once
+ * for the whole set instead of once per RT.
+ */
+void bgp_evpn_configure_import_rts_for_vrf_batch(struct bgp *bgp_vrf,
+						 struct bgp_evpn_cfgd_rt_slu_head *cfgd_rts)
 {
-	bgp_evpn_vrf_rt_change_begin(bgp_vrf, true);
-	bgp_evpn_configure_import_rt_for_vrf_deferred(bgp_vrf, cfgd_rt);
-	bgp_evpn_vrf_rt_change_end(bgp_vrf, true);
+	struct bgp_evpn_rt_config *rt_config = bgp_vrf->vrf_route_target_config;
+
+	if (!bgp_evpn_cfgd_rt_slu_count(cfgd_rts))
+		return;
+
+	evpn_vrf_rt_routes_unmap(bgp_vrf);
+
+	cfgd_rt_list_add_batch(&rt_config->cfgd_import, cfgd_rts);
+
+	/* This also removes the implicit auto RT, if there was one */
+	bgp_evpn_vrf_regenerate_effective_import_rts(bgp_vrf);
+
+	evpn_vrf_rt_routes_map(bgp_vrf);
 }

 void bgp_evpn_configure_import_auto_rt_for_vrf(struct bgp *bgp_vrf,
@@ -6688,23 +6695,28 @@ void bgp_evpn_configure_import_auto_rt_for_vrf(struct bgp *bgp_vrf,
 	evpn_vrf_rt_routes_map(bgp_vrf);
 }

-void bgp_evpn_unconfigure_import_rt_for_vrf_deferred(struct bgp *bgp_vrf,
-						     const struct bgp_evpn_cfgd_rt *cfgd_rt)
+/* Remove a set of import RTs from the VRF. Every entry of cfgd_rts is
+ * freed and cfgd_rts is left empty. The VRF's routes are unmapped and
+ * remapped once for the whole set instead of once per RT.
+ */
+void bgp_evpn_unconfigure_import_rts_for_vrf_batch(struct bgp *bgp_vrf,
+						   struct bgp_evpn_cfgd_rt_slu_head *cfgd_rts)
 {
 	struct bgp_evpn_rt_config *rt_config = bgp_vrf->vrf_route_target_config;
-	struct bgp_evpn_cfgd_rt *existing;

-	existing = bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_import, cfgd_rt);
-	if (existing)
-		cfgd_rt_list_del(&rt_config->cfgd_import, existing);
-}
+	if (!bgp_evpn_cfgd_rt_slu_count(cfgd_rts))
+		return;

-void bgp_evpn_unconfigure_import_rt_for_vrf(struct bgp *bgp_vrf,
-					    const struct bgp_evpn_cfgd_rt *cfgd_rt)
-{
-	bgp_evpn_vrf_rt_change_begin(bgp_vrf, true);
-	bgp_evpn_unconfigure_import_rt_for_vrf_deferred(bgp_vrf, cfgd_rt);
-	bgp_evpn_vrf_rt_change_end(bgp_vrf, true);
+	evpn_vrf_rt_routes_unmap(bgp_vrf);
+
+	cfgd_rt_list_del_batch(&rt_config->cfgd_import, cfgd_rts);
+
+	/* This also restores the implicit auto RT if the last manual import
+	 * RT was removed
+	 */
+	bgp_evpn_vrf_regenerate_effective_import_rts(bgp_vrf);
+
+	evpn_vrf_rt_routes_map(bgp_vrf);
 }

 void bgp_evpn_unconfigure_import_auto_rt_for_vrf(struct bgp *bgp_vrf)
@@ -6723,18 +6735,25 @@ void bgp_evpn_unconfigure_import_auto_rt_for_vrf(struct bgp *bgp_vrf)
 	evpn_vrf_rt_routes_map(bgp_vrf);
 }

-void bgp_evpn_configure_export_rt_for_vrf_deferred(struct bgp *bgp_vrf,
-						   struct bgp_evpn_cfgd_rt *cfgd_rt)
+/* Add a set of export RTs to the VRF, taking ownership of every entry and
+ * leaving cfgd_rts empty. The VRF's routes are re-advertised once for the
+ * whole set instead of once per RT.
+ */
+void bgp_evpn_configure_export_rts_for_vrf_batch(struct bgp *bgp_vrf,
+						 struct bgp_evpn_cfgd_rt_slu_head *cfgd_rts)
 {
 	struct bgp_evpn_rt_config *rt_config = bgp_vrf->vrf_route_target_config;

-	cfgd_rt_list_add(&rt_config->cfgd_export, cfgd_rt);
-}
+	if (!bgp_evpn_cfgd_rt_slu_count(cfgd_rts))
+		return;

-void bgp_evpn_configure_export_rt_for_vrf(struct bgp *bgp_vrf, struct bgp_evpn_cfgd_rt *cfgd_rt)
-{
-	bgp_evpn_configure_export_rt_for_vrf_deferred(bgp_vrf, cfgd_rt);
-	bgp_evpn_vrf_rt_change_end(bgp_vrf, false);
+	cfgd_rt_list_add_batch(&rt_config->cfgd_export, cfgd_rts);
+
+	/* This also removes the implicit auto RT, if there was one */
+	bgp_evpn_vrf_regenerate_effective_export_rts(bgp_vrf);
+
+	if (is_l3vni_live(bgp_vrf))
+		bgp_evpn_handle_export_rt_change_for_vrf(bgp_vrf);
 }

 void bgp_evpn_configure_export_auto_rt_for_vrf(struct bgp *bgp_vrf,
@@ -6755,22 +6774,27 @@ void bgp_evpn_configure_export_auto_rt_for_vrf(struct bgp *bgp_vrf,
 	bgp_evpn_handle_export_rt_change_for_vrf(bgp_vrf);
 }

-void bgp_evpn_unconfigure_export_rt_for_vrf_deferred(struct bgp *bgp_vrf,
-						     const struct bgp_evpn_cfgd_rt *cfgd_rt)
+/* Remove a set of export RTs from the VRF. Every entry of cfgd_rts is
+ * freed and cfgd_rts is left empty. The VRF's routes are re-advertised
+ * once for the whole set instead of once per RT.
+ */
+void bgp_evpn_unconfigure_export_rts_for_vrf_batch(struct bgp *bgp_vrf,
+						   struct bgp_evpn_cfgd_rt_slu_head *cfgd_rts)
 {
 	struct bgp_evpn_rt_config *rt_config = bgp_vrf->vrf_route_target_config;
-	struct bgp_evpn_cfgd_rt *existing;

-	existing = bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_export, cfgd_rt);
-	if (existing)
-		cfgd_rt_list_del(&rt_config->cfgd_export, existing);
-}
+	if (!bgp_evpn_cfgd_rt_slu_count(cfgd_rts))
+		return;

-void bgp_evpn_unconfigure_export_rt_for_vrf(struct bgp *bgp_vrf,
-					    const struct bgp_evpn_cfgd_rt *cfgd_rt)
-{
-	bgp_evpn_unconfigure_export_rt_for_vrf_deferred(bgp_vrf, cfgd_rt);
-	bgp_evpn_vrf_rt_change_end(bgp_vrf, false);
+	cfgd_rt_list_del_batch(&rt_config->cfgd_export, cfgd_rts);
+
+	/* This also restores the implicit auto RT if the last manual export
+	 * RT was removed
+	 */
+	bgp_evpn_vrf_regenerate_effective_export_rts(bgp_vrf);
+
+	if (is_l3vni_live(bgp_vrf))
+		bgp_evpn_handle_export_rt_change_for_vrf(bgp_vrf);
 }

 void bgp_evpn_unconfigure_export_auto_rt_for_vrf(struct bgp *bgp_vrf)
diff --git a/bgpd/bgp_evpn_private.h b/bgpd/bgp_evpn_private.h
index 378ad17ca2..85167f89cd 100644
--- a/bgpd/bgp_evpn_private.h
+++ b/bgpd/bgp_evpn_private.h
@@ -995,29 +995,21 @@ extern void bgp_evpn_install_uninstall_default_route(struct bgp *bgp_vrf, afi_t
 						     struct bgp_path_info *originator, bool add);
 extern void bgp_evpn_l2vni_regenerate_effective_import_rts(struct bgp *bgp, struct bgpevpn *vpn);
 extern void bgp_evpn_l2vni_regenerate_effective_export_rts(struct bgp *bgp, struct bgpevpn *vpn);
-extern void bgp_evpn_vrf_rt_change_begin(struct bgp *bgp_vrf, bool is_import);
-extern void bgp_evpn_vrf_rt_change_end(struct bgp *bgp_vrf, bool is_import);
-extern void bgp_evpn_configure_export_rt_for_vrf(struct bgp *bgp_vrf,
-						 struct bgp_evpn_cfgd_rt *cfgd_rt);
-extern void bgp_evpn_configure_export_rt_for_vrf_deferred(struct bgp *bgp_vrf,
-							  struct bgp_evpn_cfgd_rt *cfgd_rt);
+extern void bgp_evpn_configure_export_rts_for_vrf_batch(struct bgp *bgp_vrf,
+							struct bgp_evpn_cfgd_rt_slu_head *cfgd_rts);
 extern void bgp_evpn_configure_export_auto_rt_for_vrf(struct bgp *bgp_vrf,
 						      enum bgp_evpn_autort_cfgd autort);
-extern void bgp_evpn_unconfigure_export_rt_for_vrf(struct bgp *bgp_vrf,
-						   const struct bgp_evpn_cfgd_rt *cfgd_rt);
-extern void bgp_evpn_unconfigure_export_rt_for_vrf_deferred(struct bgp *bgp_vrf,
-							    const struct bgp_evpn_cfgd_rt *cfgd_rt);
+extern void
+bgp_evpn_unconfigure_export_rts_for_vrf_batch(struct bgp *bgp_vrf,
+					      struct bgp_evpn_cfgd_rt_slu_head *cfgd_rts);
 extern void bgp_evpn_unconfigure_export_auto_rt_for_vrf(struct bgp *bgp_vrf);
-extern void bgp_evpn_configure_import_rt_for_vrf(struct bgp *bgp_vrf,
-						 struct bgp_evpn_cfgd_rt *cfgd_rt);
-extern void bgp_evpn_configure_import_rt_for_vrf_deferred(struct bgp *bgp_vrf,
-							  struct bgp_evpn_cfgd_rt *cfgd_rt);
+extern void bgp_evpn_configure_import_rts_for_vrf_batch(struct bgp *bgp_vrf,
+							struct bgp_evpn_cfgd_rt_slu_head *cfgd_rts);
 extern void bgp_evpn_configure_import_auto_rt_for_vrf(struct bgp *bgp_vrf,
 						      enum bgp_evpn_autort_cfgd autort);
-extern void bgp_evpn_unconfigure_import_rt_for_vrf(struct bgp *bgp_vrf,
-						   const struct bgp_evpn_cfgd_rt *cfgd_rt);
-extern void bgp_evpn_unconfigure_import_rt_for_vrf_deferred(struct bgp *bgp_vrf,
-							    const struct bgp_evpn_cfgd_rt *cfgd_rt);
+extern void
+bgp_evpn_unconfigure_import_rts_for_vrf_batch(struct bgp *bgp_vrf,
+					      struct bgp_evpn_cfgd_rt_slu_head *cfgd_rts);
 extern void bgp_evpn_unconfigure_import_auto_rt_for_vrf(struct bgp *bgp_vrf);
 extern int bgp_evpn_handle_export_rt_change(struct bgp *bgp,
 					    struct bgpevpn *vpn);
diff --git a/bgpd/bgp_evpn_vty.c b/bgpd/bgp_evpn_vty.c
index 00880c1eea..46aff14dbf 100644
--- a/bgpd/bgp_evpn_vty.c
+++ b/bgpd/bgp_evpn_vty.c
@@ -6965,64 +6965,49 @@ DEFUN (show_bgp_vrf_l3vni_info,
 	return CMD_SUCCESS;
 }

-static void vrf_rt_import_change_start(struct bgp *bgp, bool *import_started)
-{
-	if (*import_started)
-		return;
-
-	bgp_evpn_vrf_rt_change_begin(bgp, true);
-	*import_started = true;
-}
-
-/* Add a manual route target to the VRF (L3VNI) configuration. Takes
- * ownership of cfgd_rt on success. "both" is an alias for import plus
- * export. Mutations are deferred so the caller can rebuild once per list.
+/* Queue a manual route target for addition to the VRF (L3VNI)
+ * configuration. Takes ownership of cfgd_rt on success. "both" is an
+ * alias for import plus export. An RT queued by an earlier entry of the
+ * same list counts as configured.
  */
 static int vrf_rt_add(struct bgp *bgp, struct bgp_evpn_cfgd_rt *cfgd_rt,
-		      enum bgp_evpn_rt_direction rt_direction, bool *import_started,
-		      bool *export_started)
+		      enum bgp_evpn_rt_direction rt_direction,
+		      struct bgp_evpn_cfgd_rt_slu_head *import_rts,
+		      struct bgp_evpn_cfgd_rt_slu_head *export_rts)
 {
 	struct bgp_evpn_rt_config *rt_config = bgp->vrf_route_target_config;
 	bool have_import, have_export;

+	have_import = bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_import, cfgd_rt) ||
+		      bgp_evpn_cfgd_rt_slu_find(import_rts, cfgd_rt);
+	have_export = bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_export, cfgd_rt) ||
+		      bgp_evpn_cfgd_rt_slu_find(export_rts, cfgd_rt);
+
 	/* Do nothing if we already have this route-target */
 	switch (rt_direction) {
 	case RT_TYPE_IMPORT:
-		if (bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_import, cfgd_rt))
+		if (have_import)
 			return -1;

-		vrf_rt_import_change_start(bgp, import_started);
-		bgp_evpn_configure_import_rt_for_vrf_deferred(bgp, cfgd_rt);
+		bgp_evpn_cfgd_rt_slu_add(import_rts, cfgd_rt);
 		break;
 	case RT_TYPE_EXPORT:
-		if (bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_export, cfgd_rt))
+		if (have_export)
 			return -1;

-		bgp_evpn_configure_export_rt_for_vrf_deferred(bgp, cfgd_rt);
-		/* Export begin() is a no-op; only end() re-advertises. */
-		*export_started = true;
+		bgp_evpn_cfgd_rt_slu_add(export_rts, cfgd_rt);
 		break;
 	case RT_TYPE_BOTH:
-		have_import = !!bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_import, cfgd_rt);
-		have_export = !!bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_export, cfgd_rt);
-
 		if (have_import && have_export)
 			return -1;

-		if (!have_import) {
-			vrf_rt_import_change_start(bgp, import_started);
-			if (!have_export) {
-				bgp_evpn_configure_import_rt_for_vrf_deferred(bgp, cfgd_rt);
-				bgp_evpn_configure_export_rt_for_vrf_deferred(bgp,
-									      bgp_evpn_cfgd_rt_dup(
-										      cfgd_rt));
-				*export_started = true;
-			} else {
-				bgp_evpn_configure_import_rt_for_vrf_deferred(bgp, cfgd_rt);
-			}
+		if (!have_import && !have_export) {
+			bgp_evpn_cfgd_rt_slu_add(import_rts, cfgd_rt);
+			bgp_evpn_cfgd_rt_slu_add(export_rts, bgp_evpn_cfgd_rt_dup(cfgd_rt));
+		} else if (!have_import) {
+			bgp_evpn_cfgd_rt_slu_add(import_rts, cfgd_rt);
 		} else {
-			bgp_evpn_configure_export_rt_for_vrf_deferred(bgp, cfgd_rt);
-			*export_started = true;
+			bgp_evpn_cfgd_rt_slu_add(export_rts, cfgd_rt);
 		}
 		break;
 	default:
@@ -7032,48 +7017,49 @@ static int vrf_rt_add(struct bgp *bgp, struct bgp_evpn_cfgd_rt *cfgd_rt,
 	return 0;
 }

-/* Remove a manual route target from the VRF (L3VNI) configuration.
- * The caller keeps ownership of cfgd_rt. "both" removes whichever of
- * the two directions is configured.
+/* Queue a manual route target for removal from the VRF (L3VNI)
+ * configuration. Takes ownership of cfgd_rt on success. "both" removes
+ * whichever of the two directions is configured. An RT queued by an
+ * earlier entry of the same list counts as no longer configured.
  */
-static int vrf_rt_del(struct bgp *bgp, const struct bgp_evpn_cfgd_rt *cfgd_rt,
-		      enum bgp_evpn_rt_direction rt_direction, bool *import_started,
-		      bool *export_started)
+static int vrf_rt_del(struct bgp *bgp, struct bgp_evpn_cfgd_rt *cfgd_rt,
+		      enum bgp_evpn_rt_direction rt_direction,
+		      struct bgp_evpn_cfgd_rt_slu_head *import_rts,
+		      struct bgp_evpn_cfgd_rt_slu_head *export_rts)
 {
 	struct bgp_evpn_rt_config *rt_config = bgp->vrf_route_target_config;
 	bool have_import, have_export;

+	have_import = bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_import, cfgd_rt) &&
+		      !bgp_evpn_cfgd_rt_slu_find(import_rts, cfgd_rt);
+	have_export = bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_export, cfgd_rt) &&
+		      !bgp_evpn_cfgd_rt_slu_find(export_rts, cfgd_rt);
+
 	/* Verify we already have this route-target */
 	switch (rt_direction) {
 	case RT_TYPE_IMPORT:
-		if (!bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_import, cfgd_rt))
+		if (!have_import)
 			return -1;

-		vrf_rt_import_change_start(bgp, import_started);
-		bgp_evpn_unconfigure_import_rt_for_vrf_deferred(bgp, cfgd_rt);
+		bgp_evpn_cfgd_rt_slu_add(import_rts, cfgd_rt);
 		break;
 	case RT_TYPE_EXPORT:
-		if (!bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_export, cfgd_rt))
+		if (!have_export)
 			return -1;

-		bgp_evpn_unconfigure_export_rt_for_vrf_deferred(bgp, cfgd_rt);
-		/* Export begin() is a no-op; only end() re-advertises. */
-		*export_started = true;
+		bgp_evpn_cfgd_rt_slu_add(export_rts, cfgd_rt);
 		break;
 	case RT_TYPE_BOTH:
-		have_import = !!bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_import, cfgd_rt);
-		have_export = !!bgp_evpn_cfgd_rt_slu_find(&rt_config->cfgd_export, cfgd_rt);
-
 		if (!have_import && !have_export)
 			return -1;

-		if (have_import) {
-			vrf_rt_import_change_start(bgp, import_started);
-			bgp_evpn_unconfigure_import_rt_for_vrf_deferred(bgp, cfgd_rt);
-		}
-		if (have_export) {
-			bgp_evpn_unconfigure_export_rt_for_vrf_deferred(bgp, cfgd_rt);
-			*export_started = true;
+		if (have_import && have_export) {
+			bgp_evpn_cfgd_rt_slu_add(import_rts, cfgd_rt);
+			bgp_evpn_cfgd_rt_slu_add(export_rts, bgp_evpn_cfgd_rt_dup(cfgd_rt));
+		} else if (have_import) {
+			bgp_evpn_cfgd_rt_slu_add(import_rts, cfgd_rt);
+		} else {
+			bgp_evpn_cfgd_rt_slu_add(export_rts, cfgd_rt);
 		}
 		break;
 	default:
@@ -7262,10 +7248,11 @@ static int vrf_process_rtlist_internal(struct bgp *bgp, struct vty *vty, struct
 				       int n_rts, struct bgp_evpn_cfgd_rt **cfgd_rts, bool is_add,
 				       enum bgp_evpn_rt_direction rt_direction)
 {
+	struct bgp_evpn_cfgd_rt_slu_head import_rts, export_rts;
 	int ret = CMD_SUCCESS;
-	/* True once at least one RT mutated that direction; end() then rebuilds once. */
-	bool import_started = false;
-	bool export_started = false;
+
+	bgp_evpn_cfgd_rt_slu_init(&import_rts);
+	bgp_evpn_cfgd_rt_slu_init(&export_rts);

 	for (int i = 0; i < n_rts; i++) {
 		struct bgp_evpn_cfgd_rt *cfgd_rt = cfgd_rts[i];
@@ -7273,8 +7260,7 @@ static int vrf_process_rtlist_internal(struct bgp *bgp, struct vty *vty, struct
 		cfgd_rts[i] = NULL;

 		if (is_add) {
-			if (vrf_rt_add(bgp, cfgd_rt, rt_direction, &import_started,
-				       &export_started) != 0) {
+			if (vrf_rt_add(bgp, cfgd_rt, rt_direction, &import_rts, &export_rts) != 0) {
 				vty_out(vty,
 					"%% RT specified already configured for this VRF: %s\n",
 					rt_argv[i]->arg);
@@ -7283,22 +7269,29 @@ static int vrf_process_rtlist_internal(struct bgp *bgp, struct vty *vty, struct
 			}

 		} else {
-			if (vrf_rt_del(bgp, cfgd_rt, rt_direction, &import_started,
-				       &export_started) != 0) {
+			if (vrf_rt_del(bgp, cfgd_rt, rt_direction, &import_rts, &export_rts) != 0) {
 				vty_out(vty,
 					"%% RT specified does not match configuration for this VRF: %s\n",
 					rt_argv[i]->arg);
+				bgp_evpn_cfgd_rt_free(cfgd_rt);
 				ret = CMD_WARNING;
 			}
-
-			bgp_evpn_cfgd_rt_free(cfgd_rt);
 		}
 	}

-	if (import_started)
-		bgp_evpn_vrf_rt_change_end(bgp, true);
-	if (export_started)
-		bgp_evpn_vrf_rt_change_end(bgp, false);
+	/* Hand each direction's RTs to EVPN as one set, so the VRF's routes
+	 * are rebuilt once per direction instead of once per RT.
+	 */
+	if (is_add) {
+		bgp_evpn_configure_import_rts_for_vrf_batch(bgp, &import_rts);
+		bgp_evpn_configure_export_rts_for_vrf_batch(bgp, &export_rts);
+	} else {
+		bgp_evpn_unconfigure_import_rts_for_vrf_batch(bgp, &import_rts);
+		bgp_evpn_unconfigure_export_rts_for_vrf_batch(bgp, &export_rts);
+	}
+
+	bgp_evpn_cfgd_rt_slu_fini(&import_rts);
+	bgp_evpn_cfgd_rt_slu_fini(&export_rts);

 	return ret;
 }