From 958340e935350f840e31a0405b492e6ac7dfe13b Mon Sep 17 00:00:00 2001 From: Francois Dumontet Date: Tue, 11 Jul 2023 10:03:04 +0200 Subject: [PATCH 1/3] bgpd: add set as-path exclude acl-list command A route-map applied on incoming BGP updates is not able to replace an unwanted as segments by another one. unwanted as segment are based on an AS path access-list. The below configuration illustrates the case: router bgp 65001 address-family ipv4 unicast neighbor 192.168.1.2 route-map rule_2 in exit-address-family bgp as-path access-list RULE permit ^65 route-map rule_2 permit 10 set as-path replace as-path-access-list RULE 6000 ``` BGP routing table entry for 10.10.10.10/32, version 13 Paths: (1 available, best #1, table default) Advertised to non peer-group peers: 192.168.10.65 65000 1 2 3 123 192.168.10.65 from 192.168.10.65 (10.10.10.11) Origin IGP, metric 0, valid, external, best (First path received) ``` After: ``` do show ip bgp 10.10.10.10/32 BGP routing table entry for 10.10.10.10/32, version 15 Paths: (1 available, best #1, table default) Advertised to non peer-group peers: 192.168.10.65 6000 1 2 3 123 192.168.10.65 from 192.168.10.65 (10.10.10.11) Origin IGP, metric 0, valid, external, best (First path received) ``` Signed-off-by: Francois Dumontet --- bgpd/bgp_aspath.c | 40 ++++++++++++++ bgpd/bgp_aspath.h | 3 + bgpd/bgp_routemap.c | 130 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 166 insertions(+), 7 deletions(-) diff --git a/bgpd/bgp_aspath.c b/bgpd/bgp_aspath.c index 4ea81216bf..2e2248cd72 100644 --- a/bgpd/bgp_aspath.c +++ b/bgpd/bgp_aspath.c @@ -1231,6 +1231,46 @@ bool aspath_private_as_check(struct aspath *aspath) return true; } +/* Replace all ASN instances of the regex rule with our own ASN */ +struct aspath *aspath_replace_regex_asn(struct aspath *aspath, + struct as_list *acl_list, as_t our_asn) +{ + struct aspath *new; + struct assegment *cur_seg; + struct as_list *cur_as_list; + struct as_filter *cur_as_filter; + char str_buf[ASPATH_STR_DEFAULT_LEN]; + uint32_t i; + + new = aspath_dup(aspath); + cur_seg = new->segments; + + while (cur_seg) { + cur_as_list = acl_list; + while (cur_as_list) { + cur_as_filter = cur_as_list->head; + while (cur_as_filter) { + for (i = 0; i < cur_seg->length; i++) { + snprintfrr(str_buf, + ASPATH_STR_DEFAULT_LEN, + ASN_FORMAT(new->asnotation), + &cur_seg->as[i]); + if (!regexec(cur_as_filter->reg, + str_buf, 0, NULL, 0)) + cur_seg->as[i] = our_asn; + } + cur_as_filter = cur_as_filter->next; + } + cur_as_list = cur_as_list->next; + } + cur_seg = cur_seg->next; + } + + aspath_str_update(new, false); + return new; +} + + /* Replace all instances of the target ASN with our own ASN */ struct aspath *aspath_replace_specific_asn(struct aspath *aspath, as_t target_asn, as_t our_asn) diff --git a/bgpd/bgp_aspath.h b/bgpd/bgp_aspath.h index b1a61d5b99..ebfc7d087d 100644 --- a/bgpd/bgp_aspath.h +++ b/bgpd/bgp_aspath.h @@ -107,6 +107,9 @@ extern unsigned int aspath_get_last_as(struct aspath *aspath); extern int aspath_loop_check(struct aspath *aspath, as_t asno); extern int aspath_loop_check_confed(struct aspath *aspath, as_t asno); extern bool aspath_private_as_check(struct aspath *aspath); +extern struct aspath *aspath_replace_regex_asn(struct aspath *aspath, + struct as_list *acl_list, + as_t our_asn); extern struct aspath *aspath_replace_specific_asn(struct aspath *aspath, as_t target_asn, as_t our_asn); diff --git a/bgpd/bgp_routemap.c b/bgpd/bgp_routemap.c index b7ac976e23..a8e6310187 100644 --- a/bgpd/bgp_routemap.c +++ b/bgpd/bgp_routemap.c @@ -2410,11 +2410,16 @@ route_set_aspath_replace(void *rule, const struct prefix *dummy, void *object) as_t configured_asn; char *buf; char src_asn[ASN_STRING_MAX_SIZE]; + char *acl_list_name = NULL; + uint32_t acl_list_name_len = 0; + char *buf_acl_name = NULL; + static const char asp_acl[] = "as-path-access-list"; + struct as_list *aspath_acl = NULL; if (path->peer->sort != BGP_PEER_EBGP) { zlog_warn( "`set as-path replace` is supported only for EBGP peers"); - return RMAP_NOOP; + goto end_ko; } buf = strchr(replace, ' '); @@ -2422,6 +2427,46 @@ route_set_aspath_replace(void *rule, const struct prefix *dummy, void *object) configured_asn = path->peer->change_local_as ? path->peer->change_local_as : path->peer->local_as; + } else if (!strncmp(replace, asp_acl, strlen(asp_acl))) { + /* its as-path-acl-list command get the access list name */ + while (*buf == ' ') + buf++; + buf_acl_name = buf; + buf = strchr(buf_acl_name, ' '); + if (buf) + acl_list_name_len = buf - buf_acl_name; + else + acl_list_name_len = strlen(buf_acl_name); + + buf_acl_name[acl_list_name_len] = 0; + /* get the acl-list */ + aspath_acl = as_list_lookup(buf_acl_name); + if (!aspath_acl) { + zlog_warn("`set as-path replace`, invalid as-path-access-list name: %s", + buf_acl_name); + goto end_ko; + } + acl_list_name = XSTRDUP(MTYPE_TMP, buf_acl_name); + buf_acl_name[acl_list_name_len] = ' '; + + if (!buf) { + configured_asn = path->peer->change_local_as + ? path->peer->change_local_as + : path->peer->local_as; + } else { + while (*buf == ' ') + buf++; + /* get the configured asn */ + if (!asn_str2asn(buf, &configured_asn)) { + zlog_warn( + "`set as-path replace`, invalid configured AS %s", + buf); + goto end_ko; + } + } + + replace = buf; + } else { memcpy(src_asn, replace, (size_t)(buf - replace)); src_asn[(size_t)(buf - replace)] = '\0'; @@ -2431,13 +2476,14 @@ route_set_aspath_replace(void *rule, const struct prefix *dummy, void *object) zlog_warn( "`set as-path replace`, invalid configured AS %s", buf); - return RMAP_NOOP; + goto end_ko; } } - if (!strmatch(replace, "any") && !asn_str2asn(replace, &replace_asn)) { + if (replace && !strmatch(replace, "any") && + !asn_str2asn(replace, &replace_asn)) { zlog_warn("`set as-path replace`, invalid AS %s", replace); - return RMAP_NOOP; + goto end_ko; } if (path->attr->aspath->refcnt) @@ -2445,16 +2491,29 @@ route_set_aspath_replace(void *rule, const struct prefix *dummy, void *object) else aspath_new = path->attr->aspath; - if (strmatch(replace, "any")) { + if (aspath_acl) { + path->attr->aspath = aspath_replace_regex_asn(aspath_new, + aspath_acl, + configured_asn); + } else if (strmatch(replace, "any")) { path->attr->aspath = aspath_replace_all_asn(aspath_new, configured_asn); - } else + } else { path->attr->aspath = aspath_replace_specific_asn( aspath_new, replace_asn, configured_asn); - + } aspath_free(aspath_new); + + if (acl_list_name) + XFREE(MTYPE_TMP, acl_list_name); return RMAP_OKAY; + +end_ko: + if (acl_list_name) + XFREE(MTYPE_TMP, acl_list_name); + return RMAP_NOOP; + } static const struct route_map_rule_cmd route_set_aspath_replace_cmd = { @@ -6087,6 +6146,61 @@ DEFPY_YANG(no_set_aspath_replace_asn, no_set_aspath_replace_asn_cmd, return nb_cli_apply_changes(vty, NULL); } +DEFPY_YANG( + set_aspath_replace_access_list, set_aspath_replace_access_list_cmd, + "set as-path replace as-path-access-list AS_PATH_FILTER_NAME$aspath_filter_name [$configured_asn]", + SET_STR + "Transform BGP AS-path attribute\n" + "Replace AS number to local or configured AS number\n" + "Specify an as path access list name\n" + "AS path access list name\n" + "Define the configured AS number\n") +{ + char *str; + const char *xpath = + "./set-action[action='frr-bgp-route-map:as-path-replace']"; + char xpath_value[XPATH_MAXLEN]; + as_t as_configured_value; + char replace_value[ASN_STRING_MAX_SIZE * 2]; + + if (configured_asn_str && + !asn_str2asn(configured_asn_str, &as_configured_value)) { + vty_out(vty, "%% Invalid AS configured value %s\n", + configured_asn_str); + return CMD_WARNING_CONFIG_FAILED; + } + + str = argv_concat(argv, argc, 3); + + nb_cli_enqueue_change(vty, xpath, NB_OP_CREATE, NULL); + + snprintf(replace_value, sizeof(replace_value), "%s %s", aspath_filter_name, str); + + snprintf(xpath_value, sizeof(xpath_value), + "%s/rmap-set-action/frr-bgp-route-map:replace-as-path", xpath); + nb_cli_enqueue_change(vty, xpath_value, NB_OP_MODIFY, str); + + return nb_cli_apply_changes(vty, NULL); +} + +DEFPY_YANG( + no_set_aspath_replace_access_list, no_set_aspath_replace_access_list_cmd, + "no set as-path replace as-path-access-list [AS_PATH_FILTER_NAME] [$configured_asn]", + NO_STR + SET_STR + "Transform BGP AS_PATH attribute\n" + "Replace AS number to local or configured AS number\n" + "Specify an as path access list name\n" + "AS path access list name\n" + "Define the configured AS number\n") +{ + const char *xpath = + "./set-action[action='frr-bgp-route-map:as-path-replace']"; + + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); +} + DEFUN_YANG (no_set_aspath_prepend, no_set_aspath_prepend_cmd, "no set as-path prepend [ASNUM]", @@ -7792,12 +7906,14 @@ void bgp_route_map_init(void) install_element(RMAP_NODE, &set_aspath_exclude_all_cmd); install_element(RMAP_NODE, &set_aspath_exclude_access_list_cmd); install_element(RMAP_NODE, &set_aspath_replace_asn_cmd); + install_element(RMAP_NODE, &set_aspath_replace_access_list_cmd); install_element(RMAP_NODE, &no_set_aspath_prepend_cmd); install_element(RMAP_NODE, &no_set_aspath_prepend_lastas_cmd); install_element(RMAP_NODE, &no_set_aspath_exclude_cmd); install_element(RMAP_NODE, &no_set_aspath_exclude_all_cmd); install_element(RMAP_NODE, &no_set_aspath_exclude_access_list_cmd); install_element(RMAP_NODE, &no_set_aspath_replace_asn_cmd); + install_element(RMAP_NODE, &no_set_aspath_replace_access_list_cmd); install_element(RMAP_NODE, &set_origin_cmd); install_element(RMAP_NODE, &no_set_origin_cmd); install_element(RMAP_NODE, &set_atomic_aggregate_cmd); From 459e1cd903101b533b31254791a1b099a24a5527 Mon Sep 17 00:00:00 2001 From: Francois Dumontet Date: Mon, 17 Jul 2023 17:48:49 +0200 Subject: [PATCH 2/3] tests: add one test to bgp_set_aspath_replace.py add support of set as-path replace as-path-access-list Signed-off-by: Francois Dumontet --- .../test_bgp_set_aspath_replace.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/topotests/bgp_set_aspath_replace/test_bgp_set_aspath_replace.py b/tests/topotests/bgp_set_aspath_replace/test_bgp_set_aspath_replace.py index 0433c15e0a..c0e19fa356 100644 --- a/tests/topotests/bgp_set_aspath_replace/test_bgp_set_aspath_replace.py +++ b/tests/topotests/bgp_set_aspath_replace/test_bgp_set_aspath_replace.py @@ -120,6 +120,82 @@ def test_bgp_set_aspath_replace_test2(): ), "Failed overriding incoming AS-PATH with route-map replace with configured ASN" +def test_bgp_set_aspath_replace_access_list(): + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + rname = "r1" + r1 = tgen.gears[rname] + + r1.vtysh_cmd( + """ +conf + bgp as-path access-list FIRST permit ^65 + route-map r2 permit 20 + set as-path replace as-path-access-list FIRST 65002 + """ + ) + + expected = { + "routes": { + "172.16.255.31/32": [{"path": "65002 65500"}], + "172.16.255.32/32": [{"path": "65002 65002"}], + } + } + + def _bgp_regexp_1(router): + output = json.loads(router.vtysh_cmd("show bgp ipv4 unicast json")) + + return topotest.json_cmp(output, expected) + + test_func = functools.partial(_bgp_regexp_1, tgen.gears["r1"]) + _, result = topotest.run_and_expect(test_func, None, count=30, wait=0.5) + + assert result is None, "Failed overriding incoming AS-PATH with regex 1 route-map" + r1.vtysh_cmd( + """ +conf + bgp as-path access-list SECOND permit 2 + route-map r2 permit 10 + set as-path replace as-path-access-list SECOND 65001 + """ + ) + + expected = { + "routes": { + "172.16.255.31/32": [{"path": "65001 65003"}], + "172.16.255.32/32": [{"path": "65002 65002"}], + } + } + + test_func = functools.partial(_bgp_regexp_1, tgen.gears["r1"]) + _, result = topotest.run_and_expect(test_func, None, count=30, wait=0.5) + + assert result is None, "Failed overriding incoming AS-PATH with regex 2 route-map" + + r1.vtysh_cmd( + """ +conf + bgp as-path access-list TER permit 3 + route-map r2 permit 10 + set as-path replace as-path-access-list TER + """ + ) + expected = { + "routes": { + "172.16.255.31/32": [{"path": "65002 65001"}], + "172.16.255.32/32": [{"path": "65002 65002"}], + } + } + + test_func = functools.partial(_bgp_regexp_1, tgen.gears["r1"]) + _, result = topotest.run_and_expect(test_func, None, count=30, wait=0.5) + + assert result is None, "Failed overriding incoming AS-PATH with regex 3 route-map" + + if __name__ == "__main__": args = ["-s"] + sys.argv[1:] sys.exit(pytest.main(args)) From 7ac7cd804bef2415cf2542b78ee1664020db9c6b Mon Sep 17 00:00:00 2001 From: Francois Dumontet Date: Fri, 21 Jul 2023 15:38:21 +0200 Subject: [PATCH 3/3] doc: set as-path replace as-path-access-list Signed-off-by: Francois Dumontet --- doc/user/bgp.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/user/bgp.rst b/doc/user/bgp.rst index 09173ab2f5..651aa36d0e 100644 --- a/doc/user/bgp.rst +++ b/doc/user/bgp.rst @@ -2142,6 +2142,14 @@ Using AS Path in Route Map ``any`` replaces each AS number in the AS-PATH with either the local AS number or the configured AS number. +.. clicmd:: set as-path replace as-path-access-list WORD [] + + Replace some AS numbers from the AS_PATH of the BGP path's NLRI. Substituted + AS numbers are conformant with the regex defined in as-path access-list + WORD. Changed AS numbers are replaced either by the local AS number or the + configured AS number. + The no form of this command removes this set operation from the route-map. + .. clicmd:: set as-path exclude all Remove all AS numbers from the AS_PATH of the BGP path's NLRI. The no form of