Commit 4adc31d14b for frr
commit 4adc31d14b22d0e081e90086abdcb6f008a83d76
Author: Enke Chen <enchen@paloaltonetworks.com>
Date: Wed Oct 21 16:03:19 2026 -0700
bgpd: fix default-originate with set-only route-map
When default-originate is configured with a route-map that has only
"set" clauses (no "match" clauses), the default route should be
originated unconditionally with the set clauses applied.
Previously, the code would walk the entire BGP table looking for a
route that matches the route-map. If the table was empty, no match
would be found and the default route would not be originated, even
though a set-only route-map should always permit.
Fix by counting the match rules in the route-map. If there are no
match rules (and no "call" statements to other maps that might have
conditions), skip the table walk and apply the set clauses directly
to the route attributes.
This fix also resolves a scalability issue for set-only route-maps:
previously, any configured route-map (even one with only "set" clauses)
would trigger a full BGP RIB walk on every route change. In deployments
with large routing tables, this O(N) operation per update was expensive.
By skipping the table walk for set-only route-maps, these cases now
run in O(1). Set-only route-maps are common in production deployments
where operators want to apply attributes (e.g., MED, community, or
AS-path prepend) to the default route without any conditional logic.
Implementation details:
- Count both match clauses and "call" statements - a called map may
have conditions that require the table walk.
- Pass the actual default prefix (with correct address family) to
route_map_apply() so that IPv4/IPv6 specific set operations work
correctly.
- Apply the set-only route-map to attr_copy (after the dup) rather
than new_attr to avoid memory leak - attr_copy will be properly
flushed after use.
- Add gshut community AFTER the set-only route-map so that route-map
set operations cannot overwrite the graceful-shutdown signal.
- Check the route-map result and do not advertise if the route-map
denies (e.g., "deny" action with no match clauses).
Also handle the case where a route-map is configured by name but is
undefined or empty - treat this as an implicit deny and withdraw the
default route.
For backward compatibility, introduce "bgp default-originate
set-only-unconditional" knob. By default, the old behavior is
preserved (set-only route-maps still require at least one table route).
The new behavior can be enabled with this command. The default will
flip in a future release (CONFDATE 20271101), at which point the knob
will be deprecated.
Signed-off-by: Enke Chen <enchen@paloaltonetworks.com>
diff --git a/bgpd/bgp_updgrp_adv.c b/bgpd/bgp_updgrp_adv.c
index 295eefd241..76b88e9465 100644
--- a/bgpd/bgp_updgrp_adv.c
+++ b/bgpd/bgp_updgrp_adv.c
@@ -936,6 +936,43 @@ void subgroup_announce_route(struct update_subgroup *subgrp)
UNSET_FLAG(subgrp->sflags, SUBGRP_STATUS_FORCE_UPDATES);
}
+/* Count the matching rules (and calls) in a route-map */
+static int route_map_match_rules(struct route_map *rmap)
+{
+ int count = 0;
+ struct route_map_rule *rule;
+ struct route_map_index *index;
+
+ for (index = rmap->head; index; index = index->next) {
+ /* Count match clauses */
+ for (rule = index->match_list.head; rule; rule = rule->next)
+ count++;
+ /* A "call" to another map may have conditions */
+ if (index->nextrm)
+ count++;
+ }
+
+ return count;
+}
+
+/* Perform the "set" operations in a route-map without "match" config */
+static route_map_result_t bgp_route_map_setting_only(struct bgp *bgp, struct route_map *rmap,
+ uint8_t rmap_type, const struct prefix *p,
+ struct attr *attr)
+{
+ struct bgp_path_info rmap_path = { 0 };
+ route_map_result_t ret;
+
+ rmap_path.attr = attr;
+ rmap_path.peer = bgp->peer_self;
+
+ SET_FLAG(bgp->peer_self->rmap_type, rmap_type);
+ ret = route_map_apply(rmap, p, &rmap_path);
+ bgp->peer_self->rmap_type = 0;
+
+ return ret;
+}
+
void subgroup_default_originate(struct update_subgroup *subgrp, bool withdraw)
{
struct bgp *bgp;
@@ -955,6 +992,8 @@ void subgroup_default_originate(struct update_subgroup *subgrp, bool withdraw)
safi_t safi_rib;
int pref = 65536;
int new_pref = 0;
+ int match_rules;
+ struct route_map *rmap;
if (!subgrp)
return;
@@ -1000,7 +1039,30 @@ void subgroup_default_originate(struct update_subgroup *subgrp, bool withdraw)
attr.mp_nexthop_len = IPV4_MAX_BYTELEN;
}
+ rmap = peer->default_rmap[afi][safi].map;
+
if (peer->default_rmap[afi][safi].name) {
+ if (!rmap || !rmap->head) {
+ /*
+ * Route-map is configured but undefined or empty.
+ * Treat as implicit deny - withdraw the default.
+ */
+ if (!withdraw &&
+ !CHECK_FLAG(subgrp->sflags, SUBGRP_STATUS_DEFAULT_ORIGINATE))
+ SET_FLAG(subgrp->sflags, SUBGRP_STATUS_DEFAULT_ORIGINATE);
+ withdraw = true;
+ }
+ }
+
+ match_rules = rmap ? route_map_match_rules(rmap) : 0;
+
+ /*
+ * Walk the table if the route-map has match clauses, OR if it's a
+ * set-only route-map but the bgp instance requires at least one
+ * table route (backward-compatible behavior).
+ */
+ if (rmap && (match_rules != 0 ||
+ !CHECK_FLAG(bgp->flags, BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL))) {
struct bgp_path_info tmp_pi = {0};
tmp_pi.peer = bgp->peer_self;
@@ -1138,11 +1200,6 @@ void subgroup_default_originate(struct update_subgroup *subgrp, bool withdraw)
bgp_dest_unlock_node(dest);
}
- /* Advertise the default route */
- if (bgp_in_graceful_shutdown(bgp) ||
- (CHECK_FLAG(peer->flags, PEER_FLAG_GRACEFUL_SHUTDOWN)))
- bgp_attr_add_gshut_community(new_attr);
-
/*
* Prepend local-AS for EBGP. Make a copy first since
* new_attr may be interned (shared) when route-map
@@ -1150,6 +1207,42 @@ void subgroup_default_originate(struct update_subgroup *subgrp, bool withdraw)
* the attribute hash.
*/
bgp_attr_dup_into(&attr_copy, new_attr);
+
+ /* Apply set-only route-map to attr_copy (not new_attr)
+ * to avoid memory leak - attr_copy will be flushed.
+ * Only needed when table walk was skipped (new behavior).
+ */
+ if (rmap && match_rules == 0 &&
+ CHECK_FLAG(bgp->flags,
+ BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL)) {
+ if (bgp_route_map_setting_only(bgp, rmap, PEER_RMAP_TYPE_DEFAULT,
+ &p, &attr_copy) == RMAP_DENYMATCH) {
+ /*
+ * Route-map denied - withdraw default.
+ * The flag may have been cleared by callers,
+ * so withdraw unconditionally. This may send
+ * an unnecessary withdraw, but a deny-only
+ * route-map is not a practical scenario.
+ */
+ bgp_attr_flush(&attr_copy);
+ aspath_unintern(&aspath);
+ subgroup_default_withdraw_packet(subgrp);
+ UNSET_FLAG(subgrp->sflags, SUBGRP_STATUS_DEFAULT_ORIGINATE);
+ return;
+ }
+ /* Clear reflection attributes - default-originate
+ * is self-originated, not reflected.
+ */
+ bgp_attr_clear_reflection(&attr_copy);
+ }
+
+ /* Add gshut community after set-only so route-map
+ * cannot overwrite the shutdown signal.
+ */
+ if (bgp_in_graceful_shutdown(bgp) ||
+ (CHECK_FLAG(peer->flags, PEER_FLAG_GRACEFUL_SHUTDOWN)))
+ bgp_attr_add_gshut_community(&attr_copy);
+
bgp_peer_prepend_local_as(peer, afi, safi, &attr_copy);
SET_FLAG(subgrp->sflags,
diff --git a/bgpd/bgp_vty.c b/bgpd/bgp_vty.c
index e4b90636ae..00c3160b5c 100644
--- a/bgpd/bgp_vty.c
+++ b/bgpd/bgp_vty.c
@@ -143,6 +143,13 @@ FRR_CFG_DEFAULT_BOOL(BGP_COMPARE_AIGP,
{ .val_bool = false },
);
+#if CONFDATE > 20271101
+CPP_NOTICE("Flip BGP_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL default to true")
+#endif
+FRR_CFG_DEFAULT_BOOL(BGP_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL,
+ { .val_bool = false },
+);
+
FRR_CFG_DEFAULT_BOOL(BGP_IPV6_NEXTHOP_PREFER_GLOBAL,
{ .val_bool = false },
);
@@ -774,6 +781,8 @@ int bgp_get_vty(struct bgp **bgp, as_t *as, const char *name,
SET_FLAG((*bgp)->flags, BGP_FLAG_RR_ALLOW_OUTBOUND_POLICY);
if (DFLT_BGP_COMPARE_AIGP)
SET_FLAG((*bgp)->flags, BGP_FLAG_COMPARE_AIGP);
+ if (DFLT_BGP_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL)
+ SET_FLAG((*bgp)->flags, BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL);
ret = BGP_SUCCESS;
}
@@ -3426,6 +3435,28 @@ DEFUN (no_bgp_deterministic_med,
return CMD_SUCCESS;
}
+#if CONFDATE > 20271101
+CPP_NOTICE("Flip default and deprecate `bgp default-originate set-only-unconditional` knob")
+#endif
+/* "bgp default-originate set-only-unconditional" configuration. */
+DEFPY (bgp_default_originate_setonly_empty_table,
+ bgp_default_originate_setonly_empty_table_cmd,
+ "[no$no] bgp default-originate set-only-unconditional",
+ NO_STR
+ BGP_STR
+ "Control default-originate behavior\n"
+ "Permit set-only route-maps to originate default even with empty table\n")
+{
+ VTY_DECLVAR_CONTEXT(bgp, bgp);
+
+ if (no)
+ UNSET_FLAG(bgp->flags, BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL);
+ else
+ SET_FLAG(bgp->flags, BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL);
+
+ return CMD_SUCCESS;
+}
+
static int bgp_inst_gr_config_vty(struct vty *vty, struct bgp *bgp, bool on,
bool disable)
{
@@ -22647,6 +22678,15 @@ int bgp_config_write(struct vty *vty)
? ""
: "no ");
+ /* BGP default-originate set-only-unconditional. */
+ if (!!CHECK_FLAG(bgp->flags, BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL) !=
+ SAVE_BGP_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL)
+ vty_out(vty, " %sbgp default-originate set-only-unconditional\n",
+ CHECK_FLAG(bgp->flags,
+ BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL)
+ ? ""
+ : "no ");
+
/* BGP update-delay. */
bgp_config_write_update_delay(vty, bgp);
@@ -23790,6 +23830,9 @@ void bgp_vty_init(void)
install_element(BGP_NODE, &bgp_deterministic_med_cmd);
install_element(BGP_NODE, &no_bgp_deterministic_med_cmd);
+ /* "bgp default-originate set-only-unconditional" command */
+ install_element(BGP_NODE, &bgp_default_originate_setonly_empty_table_cmd);
+
/* "bgp graceful-restart" command */
install_element(BGP_NODE, &bgp_graceful_restart_cmd);
install_element(BGP_NODE, &no_bgp_graceful_restart_cmd);
diff --git a/bgpd/bgpd.h b/bgpd/bgpd.h
index 207f341312..ae4da0b923 100644
--- a/bgpd/bgpd.h
+++ b/bgpd/bgpd.h
@@ -793,6 +793,8 @@ struct bgp {
#define BGP_FLAG_BESTPATH_USE_IMPORTED_ATTRS (1ULL << 47)
/* Shutdown without sending notification (for HA standby transition) */
#define BGP_FLAG_SHUTDOWN_NO_NOTIFY (1ULL << 48)
+/* default-originate with set-only route-map works even with empty table */
+#define BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL (1ULL << 49)
/* BGP default address-families.
* New peers inherit enabled afi/safis from bgp instance.