Commit 9f5408e938 for frr

commit 9f5408e938425c2c4eac3edd7467f8da3667c478
Author: Donatas Abraitis <donatas@opensourcerouting.org>
Date:   Wed Oct 28 16:15:51 2026 +0200

    bgpd: Count only advertised paths against PATHS-LIMIT capability

    Paths filtered by subgroup_announce_check() were included, but it might be
    filtered which is then becomes "wrong count".

    Let's check the status of subgroup_announce_check().

    Signed-off-by: Donatas Abraitis <donatas@opensourcerouting.org>

diff --git a/bgpd/bgp_route.c b/bgpd/bgp_route.c
index db14caa812..9ac79da872 100644
--- a/bgpd/bgp_route.c
+++ b/bgpd/bgp_route.c
@@ -4050,18 +4050,18 @@ void bgp_best_selection(struct bgp *bgp, struct bgp_dest *dest,

 /*
  * A new route/change in bestpath of an existing route. Evaluate the path
- * for advertisement to the subgroup.
+ * for advertisement to the subgroup. Returns true if the path is advertised.
  */
-void subgroup_process_announce_selected(struct update_subgroup *subgrp,
-					struct bgp_path_info *selected,
-					struct bgp_dest *dest, afi_t afi,
-					safi_t safi, uint32_t addpath_tx_id)
+bool subgroup_process_announce_selected(struct update_subgroup *subgrp,
+					struct bgp_path_info *selected, struct bgp_dest *dest,
+					afi_t afi, safi_t safi, uint32_t addpath_tx_id)
 {
 	const struct prefix *p;
 	struct peer *onlypeer, *peer;
 	struct attr attr = { 0 }, *pattr = &attr;
 	struct bgp *bgp;
 	bool advertise;
+	bool announced = false;

 	p = bgp_dest_get_prefix(dest);
 	bgp = SUBGRP_INST(subgrp);
@@ -4075,7 +4075,7 @@ void subgroup_process_announce_selected(struct update_subgroup *subgrp,
 	/* First update is deferred until ORF or ROUTE-REFRESH is received */
 	if (onlypeer && CHECK_FLAG(onlypeer->af_sflags[afi][safi],
 				   PEER_STATUS_ORF_WAIT_REFRESH))
-		return;
+		return false;

 	memset(&attr, 0, sizeof(attr));
 	/* It's initialized in bgp_announce_check() */
@@ -4101,6 +4101,8 @@ void subgroup_process_announce_selected(struct update_subgroup *subgrp,
 								      selected))
 						bgp_attr_flush(pattr);

+					announced = true;
+
 					/* Remove paths from Adj-RIB-Out if it's not a best (selected) path.
 					 * Why should we keep Adj-RIB-Out with stale paths?
 					 */
@@ -4124,8 +4126,10 @@ void subgroup_process_announce_selected(struct update_subgroup *subgrp,
 					bgp_adj_out_unset_subgroup(dest, subgrp, addpath_tx_id);
 					bgp_attr_flush(pattr);
 				}
-			} else
+			} else {
 				bgp_attr_flush(pattr);
+				announced = true;
+			}
 		} else {
 			bgp_adj_out_unset_subgroup(dest, subgrp, addpath_tx_id);
 			bgp_attr_flush(pattr);
@@ -4136,6 +4140,8 @@ void subgroup_process_announce_selected(struct update_subgroup *subgrp,
 	else {
 		bgp_adj_out_unset_subgroup(dest, subgrp, addpath_tx_id);
 	}
+
+	return announced;
 }

 /*
diff --git a/bgpd/bgp_route.h b/bgpd/bgp_route.h
index adeabe36bb..e8d8e77a35 100644
--- a/bgpd/bgp_route.h
+++ b/bgpd/bgp_route.h
@@ -1046,10 +1046,9 @@ extern void route_vty_out_overlay(struct vty *vty, const struct prefix *p,

 extern void bgp_notify_conditional_adv_scanner(struct update_subgroup *subgrp);

-extern void subgroup_process_announce_selected(struct update_subgroup *subgrp,
+extern bool subgroup_process_announce_selected(struct update_subgroup *subgrp,
 					       struct bgp_path_info *selected,
-					       struct bgp_dest *dest, afi_t afi,
-					       safi_t safi,
+					       struct bgp_dest *dest, afi_t afi, safi_t safi,
 					       uint32_t addpath_tx_id);

 extern bool subgroup_announce_check(struct bgp_dest *dest,
diff --git a/bgpd/bgp_updgrp_adv.c b/bgpd/bgp_updgrp_adv.c
index 91a78517b1..295eefd241 100644
--- a/bgpd/bgp_updgrp_adv.c
+++ b/bgpd/bgp_updgrp_adv.c
@@ -98,86 +98,89 @@ subgrp_announce_addpath_best_selected(struct bgp_dest *dest,
 	afi_t afi = SUBGRP_AFI(subgrp);
 	safi_t safi = SUBGRP_SAFI(subgrp);
 	struct peer *peer = SUBGRP_PEER(subgrp);
-	enum bgp_path_selection_reason reason;
-	char pfx_buf[PREFIX2STR_BUFFER] = {};
-	int paths_eq = 0;
-	struct list *list = list_new();
 	struct bgp_path_info *pi = NULL;
+	struct bgp_path_info *best = NULL;
 	uint16_t paths_count = 0;
 	uint16_t paths_limit = peer->addpath_paths_limit[afi][safi].receive;

-	if (peer->addpath_type[afi][safi] == BGP_ADDPATH_BEST_SELECTED) {
-		paths_limit =
-			paths_limit
-				? MIN(paths_limit,
-				      peer->addpath_best_selected[afi][safi])
-				: peer->addpath_best_selected[afi][safi];
+	for (pi = bgp_dest_get_bgp_path_info(dest); pi; pi = pi->next) {
+		if (CHECK_FLAG(pi->flags, BGP_PATH_SELECTED)) {
+			best = pi;
+			break;
+		}
+	}

-		while (paths_count++ < paths_limit) {
-			struct bgp_path_info *exist = NULL;
+	if (best && subgroup_process_announce_selected(subgrp, best, dest, afi, safi,
+						       bgp_addpath_id_for_peer(peer, afi, safi,
+									       &best->tx_addpath)))
+		paths_count++;

-			for (pi = bgp_dest_get_bgp_path_info(dest); pi;
-			     pi = pi->next) {
-				if (listnode_lookup(list, pi))
-					continue;
+	if (peer->addpath_type[afi][safi] == BGP_ADDPATH_BEST_SELECTED) {
+		enum bgp_path_selection_reason reason;
+		char pfx_buf[PREFIX2STR_BUFFER] = {};
+		int paths_eq = 0;
+		struct list *list = list_new();
+		uint16_t selected_limit = peer->addpath_best_selected[afi][safi];
+		uint16_t selected_count = 0;
+		uint16_t remaining = paths_limit ? paths_limit - paths_count : selected_limit;
+
+		/* Pick N next best paths excluding the bestpath */
+		while (listcount(list) < selected_limit && selected_count < remaining) {
+			struct bgp_path_info *exist = NULL;

-				if (CHECK_FLAG(pi->flags, BGP_PATH_SELECTED))
+			for (pi = bgp_dest_get_bgp_path_info(dest); pi; pi = pi->next) {
+				if (pi == best || listnode_lookup(list, pi))
 					continue;

-				if (bgp_path_info_cmp(peer->bgp, pi, exist,
-						      &paths_eq, NULL, 0,
-						      pfx_buf, afi, safi,
-						      &reason))
+				if (bgp_path_info_cmp(peer->bgp, pi, exist, &paths_eq, NULL, 0,
+						      pfx_buf, afi, safi, &reason))
 					exist = pi;
 			}

-			if (exist)
-				listnode_add(list, exist);
+			if (!exist)
+				break;
+
+			listnode_add(list, exist);
+
+			if (subgroup_process_announce_selected(
+				    subgrp, exist, dest, afi, safi,
+				    bgp_addpath_id_for_peer(peer, afi, safi, &exist->tx_addpath)))
+				selected_count++;
+		}
+
+		for (pi = bgp_dest_get_bgp_path_info(dest); pi; pi = pi->next) {
+			if (pi == best || listnode_lookup(list, pi))
+				continue;
+
+			subgroup_process_announce_selected(subgrp, NULL, dest, afi, safi,
+							   bgp_addpath_id_for_peer(peer, afi, safi,
+										   &pi->tx_addpath));
 		}
+
+		list_delete(&list);
+		return;
 	}

 	for (pi = bgp_dest_get_bgp_path_info(dest); pi; pi = pi->next) {
 		uint32_t id = bgp_addpath_id_for_peer(peer, afi, safi,
 						      &pi->tx_addpath);

-		if (peer->addpath_type[afi][safi] ==
-		    BGP_ADDPATH_BEST_SELECTED) {
-			if (listnode_lookup(list, pi))
-				subgroup_process_announce_selected(
-					subgrp, pi, dest, afi, safi, id);
-			else {
-				if (CHECK_FLAG(pi->flags, BGP_PATH_SELECTED))
-					continue;
+		if (pi == best)
+			continue;

-				subgroup_process_announce_selected(
-					subgrp, NULL, dest, afi, safi, id);
-			}
-		} else {
-			/* No Paths-Limit involved */
-			if (!paths_limit) {
-				subgroup_process_announce_selected(subgrp, pi,
-								   dest, afi,
-								   safi, id);
-				continue;
-			}
+		/* No Paths-Limit involved */
+		if (!paths_limit) {
+			subgroup_process_announce_selected(subgrp, pi, dest, afi, safi, id);
+			continue;
+		}

-			/* If we have Paths-Limit capability, we MUST
-			 * not send more than the number of paths expected
-			 * by the peer.
-			 */
-			if (paths_count++ < paths_limit)
-				subgroup_process_announce_selected(subgrp, pi,
-								   dest, afi,
-								   safi, id);
-			else
-				subgroup_process_announce_selected(subgrp, NULL,
-								   dest, afi,
-								   safi, id);
+		if (paths_count < paths_limit) {
+			if (subgroup_process_announce_selected(subgrp, pi, dest, afi, safi, id))
+				paths_count++;
+		} else {
+			subgroup_process_announce_selected(subgrp, NULL, dest, afi, safi, id);
 		}
 	}
-
-	if (list)
-		list_delete(&list);
 }

 static void subgrp_withdraw_stale_addpath(struct updwalk_context *ctx,
@@ -250,16 +253,6 @@ static int group_announce_route_walkcb(struct update_group *updgrp, void *arg)

 			subgrp_withdraw_stale_addpath(ctx, subgrp);
 			subgrp_announce_addpath_best_selected(ctx->dest, subgrp);
-
-			/* Process the bestpath last so the
-			 * "show [ip] bgp neighbor x.x.x.x advertised" output shows
-			 * the attributes from the bestpath.
-			 */
-			if (ctx->pi)
-				subgroup_process_announce_selected(
-					subgrp, ctx->pi, ctx->dest, afi, safi,
-					bgp_addpath_id_for_peer(peer, afi, safi,
-								&ctx->pi->tx_addpath));
 		} else {
 			/* Send withdrawals without waiting for coalesting timer
 			 * to expire.
@@ -842,9 +835,10 @@ void subgroup_announce_table(struct update_subgroup *subgrp,
 	SET_FLAG(subgrp->sflags, SUBGRP_STATUS_TABLE_REPARSING);

 	for (dest = bgp_table_top(table); dest; dest = bgp_route_next(dest)) {
-
-		if (addpath_capable)
+		if (addpath_capable) {
 			subgrp_announce_addpath_best_selected(dest, subgrp);
+			continue;
+		}

 		for (ri = bgp_dest_get_bgp_path_info(dest); ri; ri = ri->next) {

diff --git a/tests/topotests/bgp_addpath_paths_limit/test_bgp_addpath_paths_limit.py b/tests/topotests/bgp_addpath_paths_limit/test_bgp_addpath_paths_limit.py
index aceffc026a..f342971e1a 100644
--- a/tests/topotests/bgp_addpath_paths_limit/test_bgp_addpath_paths_limit.py
+++ b/tests/topotests/bgp_addpath_paths_limit/test_bgp_addpath_paths_limit.py
@@ -179,6 +179,165 @@ def test_bgp_pathcount_json():
     ), "pathCount in JSON output doesn't match expected value for r7"


+def _bgp_received_aspaths(router):
+    output = json.loads(router.vtysh_cmd("show bgp ipv4 unicast 172.16.16.254/32 json"))
+
+    return sorted(path["aspath"]["string"] for path in output.get("paths", []))
+
+
+def _bgp_check_received_aspaths(router, expected):
+    received = _bgp_received_aspaths(router)
+    if received != sorted(expected):
+        return "Expected {}, got {}".format(sorted(expected), received)
+
+    return None
+
+
+def test_bgp_addpath_paths_limit_filtered_paths():
+    """
+    Filtered paths must not be counted against Paths-Limit.
+    """
+    tgen = get_topogen()
+
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    r1 = tgen.gears["r1"]
+    r2 = tgen.gears["r2"]
+
+    r2.vtysh_cmd(
+        """
+    configure terminal
+     bgp as-path access-list r1-out seq 5 deny ^6500[56]$
+     bgp as-path access-list r1-out seq 10 permit .*
+     router bgp 65002
+      address-family ipv4 unicast
+       neighbor 192.168.1.1 filter-list r1-out out
+    """
+    )
+
+    test_func = functools.partial(
+        _bgp_check_received_aspaths, r1, ["65002 65004", "65002 65003"]
+    )
+    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
+    assert result is None, "Filtered paths are counted against Paths-Limit: {}".format(
+        result
+    )
+
+    r2.vtysh_cmd(
+        """
+    configure terminal
+     router bgp 65002
+      address-family ipv4 unicast
+       no neighbor 192.168.1.1 filter-list r1-out out
+     no bgp as-path access-list r1-out
+    """
+    )
+
+    test_func = functools.partial(
+        _bgp_check_received_aspaths, r1, ["65002 65006", "65002 65005"]
+    )
+    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
+    assert result is None, "r1 did not receive the two best paths: {}".format(result)
+
+
+def test_bgp_addpath_paths_limit_best_selected():
+    """
+    Bestpath must be included into Paths-Limit with addpath-tx-best-selected.
+    """
+    tgen = get_topogen()
+
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    r2 = tgen.gears["r2"]
+    r7 = tgen.gears["r7"]
+
+    r2.vtysh_cmd(
+        """
+    configure terminal
+     router bgp 65002
+      address-family ipv4 unicast
+       neighbor 192.168.7.7 addpath-tx-best-selected 3
+    """
+    )
+
+    test_func = functools.partial(
+        _bgp_check_received_aspaths,
+        r7,
+        ["65002 65006", "65002 65005", "65002 65004"],
+    )
+    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
+    assert result is None, "Bestpath is not included into Paths-Limit: {}".format(
+        result
+    )
+
+    r2.vtysh_cmd(
+        """
+    configure terminal
+     router bgp 65002
+      address-family ipv4 unicast
+       neighbor 192.168.7.7 addpath-tx-all-paths
+    """
+    )
+
+    test_func = functools.partial(
+        _bgp_check_received_aspaths,
+        r7,
+        ["65002 65006", "65002 65005", "65002 65004"],
+    )
+    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
+    assert result is None, "r7 did not receive the three best paths: {}".format(result)
+
+
+def test_bgp_addpath_paths_limit_best_selected_filtered():
+    """
+    Filtered candidate must not consume Paths-Limit with addpath-tx-best-selected.
+    """
+    tgen = get_topogen()
+
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    r1 = tgen.gears["r1"]
+    r2 = tgen.gears["r2"]
+
+    r2.vtysh_cmd(
+        """
+    configure terminal
+     bgp as-path access-list r1-out seq 5 deny ^65005$
+     bgp as-path access-list r1-out seq 10 permit .*
+     router bgp 65002
+      address-family ipv4 unicast
+       neighbor 192.168.1.1 addpath-tx-best-selected 3
+       neighbor 192.168.1.1 filter-list r1-out out
+    """
+    )
+
+    test_func = functools.partial(
+        _bgp_check_received_aspaths, r1, ["65002 65006", "65002 65004"]
+    )
+    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
+    assert result is None, "Filtered candidate consumed Paths-Limit: {}".format(result)
+
+    r2.vtysh_cmd(
+        """
+    configure terminal
+     router bgp 65002
+      address-family ipv4 unicast
+       no neighbor 192.168.1.1 filter-list r1-out out
+       neighbor 192.168.1.1 addpath-tx-all-paths
+     no bgp as-path access-list r1-out
+    """
+    )
+
+    test_func = functools.partial(
+        _bgp_check_received_aspaths, r1, ["65002 65006", "65002 65005"]
+    )
+    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
+    assert result is None, "r1 did not receive the two best paths: {}".format(result)
+
+
 if __name__ == "__main__":
     args = ["-s"] + sys.argv[1:]
     sys.exit(pytest.main(args))