diff --git a/bgpd/bgp_aspath.c b/bgpd/bgp_aspath.c index 2c0de43c9b..22c9fe0e66 100644 --- a/bgpd/bgp_aspath.c +++ b/bgpd/bgp_aspath.c @@ -1598,6 +1598,24 @@ struct aspath *aspath_filter_exclude(struct aspath *source, return newpath; } +struct aspath *aspath_filter_exclude_all(struct aspath *source) +{ + struct aspath *newpath; + + newpath = aspath_new(source->asnotation); + + aspath_str_update(newpath, false); + /* We are happy returning even an empty AS_PATH, because the + * administrator + * might expect this very behaviour. There's a mean to avoid this, if + * necessary, + * by having a match rule against certain AS_PATH regexps in the + * route-map index. + */ + aspath_free(source); + return newpath; +} + /* Add specified AS to the leftmost of aspath. */ static struct aspath *aspath_add_asns(struct aspath *aspath, as_t asno, uint8_t type, unsigned num) diff --git a/bgpd/bgp_aspath.h b/bgpd/bgp_aspath.h index 18af375c13..a3aae14f8f 100644 --- a/bgpd/bgp_aspath.h +++ b/bgpd/bgp_aspath.h @@ -76,6 +76,7 @@ extern struct aspath *aspath_aggregate(struct aspath *as1, struct aspath *as2); extern struct aspath *aspath_prepend(struct aspath *as1, struct aspath *as2); extern struct aspath *aspath_filter_exclude(struct aspath *source, struct aspath *exclude_list); +extern struct aspath *aspath_filter_exclude_all(struct aspath *source); extern struct aspath *aspath_add_seq_n(struct aspath *aspath, as_t asno, unsigned num); extern struct aspath *aspath_add_seq(struct aspath *aspath, as_t asno); diff --git a/bgpd/bgp_routemap.c b/bgpd/bgp_routemap.c index 7db110be93..d29b91b48f 100644 --- a/bgpd/bgp_routemap.c +++ b/bgpd/bgp_routemap.c @@ -2300,6 +2300,31 @@ static const struct route_map_rule_cmd route_set_aspath_prepend_cmd = { }; /* `set as-path exclude ASn' */ +struct aspath_exclude { + struct aspath *aspath; + bool exclude_all; +}; + +static void *route_aspath_exclude_compile(const char *arg) +{ + struct aspath_exclude *ase; + const char *str = arg; + + ase = XCALLOC(MTYPE_ROUTE_MAP_COMPILED, sizeof(struct aspath_exclude)); + if (!strmatch(str, "all")) + ase->aspath = aspath_str2aspath(str, bgp_get_asnotation(NULL)); + else + ase->exclude_all = true; + return ase; +} + +static void route_aspath_exclude_free(void *rule) +{ + struct aspath_exclude *ase = rule; + + aspath_free(ase->aspath); + XFREE(MTYPE_ROUTE_MAP_COMPILED, ase); +} /* For ASN exclude mechanism. * Iterate over ASns requested and filter them from the given AS_PATH one by @@ -2309,16 +2334,28 @@ static const struct route_map_rule_cmd route_set_aspath_prepend_cmd = { static enum route_map_cmd_result_t route_set_aspath_exclude(void *rule, const struct prefix *dummy, void *object) { - struct aspath *new_path, *exclude_path; + struct aspath *new_path; struct bgp_path_info *path; + struct aspath_exclude *ase = rule; - exclude_path = rule; path = object; + + if (path->peer->sort != BGP_PEER_EBGP) { + zlog_warn( + "`set as-path exclude` is supported only for EBGP peers"); + return RMAP_NOOP; + } + if (path->attr->aspath->refcnt) new_path = aspath_dup(path->attr->aspath); else new_path = path->attr->aspath; - path->attr->aspath = aspath_filter_exclude(new_path, exclude_path); + + if (ase->aspath) + path->attr->aspath = + aspath_filter_exclude(new_path, ase->aspath); + else if (ase->exclude_all) + path->attr->aspath = aspath_filter_exclude_all(new_path); return RMAP_OKAY; } @@ -2327,8 +2364,8 @@ route_set_aspath_exclude(void *rule, const struct prefix *dummy, void *object) static const struct route_map_rule_cmd route_set_aspath_exclude_cmd = { "as-path exclude", route_set_aspath_exclude, - route_aspath_compile, - route_aspath_free, + route_aspath_exclude_compile, + route_aspath_exclude_free, }; /* `set as-path replace AS-PATH` */ @@ -5910,6 +5947,32 @@ DEFUN_YANG (set_aspath_exclude, return ret; } +DEFPY_YANG(set_aspath_exclude_all, set_aspath_exclude_all_cmd, + "[no$no] set as-path exclude all$all", + NO_STR SET_STR + "Transform BGP AS-path attribute\n" + "Exclude from the as-path\n" + "Exclude all AS numbers from the as-path\n") +{ + int ret; + const char *xpath = + "./set-action[action='frr-bgp-route-map:as-path-exclude']"; + char xpath_value[XPATH_MAXLEN]; + + if (no) + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + else { + nb_cli_enqueue_change(vty, xpath, NB_OP_CREATE, NULL); + snprintf(xpath_value, sizeof(xpath_value), + "%s/rmap-set-action/frr-bgp-route-map:exclude-as-path", + xpath); + nb_cli_enqueue_change(vty, xpath_value, NB_OP_MODIFY, all); + } + ret = nb_cli_apply_changes(vty, NULL); + + return ret; +} + DEFUN_YANG (no_set_aspath_exclude, no_set_aspath_exclude_cmd, "no set as-path exclude ASNUM...", @@ -7436,6 +7499,7 @@ void bgp_route_map_init(void) install_element(RMAP_NODE, &set_aspath_prepend_asn_cmd); install_element(RMAP_NODE, &set_aspath_prepend_lastas_cmd); install_element(RMAP_NODE, &set_aspath_exclude_cmd); + install_element(RMAP_NODE, &set_aspath_exclude_all_cmd); install_element(RMAP_NODE, &set_aspath_replace_asn_cmd); install_element(RMAP_NODE, &no_set_aspath_prepend_cmd); install_element(RMAP_NODE, &no_set_aspath_prepend_lastas_cmd); diff --git a/doc/user/bgp.rst b/doc/user/bgp.rst index a2585f3a57..215f473c36 100644 --- a/doc/user/bgp.rst +++ b/doc/user/bgp.rst @@ -2106,6 +2106,11 @@ Using AS Path in Route Map Replace a specific AS number to local AS number. ``any`` replaces each AS number in the AS-PATH with the local AS number. +.. clicmd:: set as-path exclude all + + Remove all AS numbers from the AS_PATH of the BGP path's NLRI. The no form of + this command removes this set operation from the route-map. + .. _bgp-communities-attribute: Communities Attribute diff --git a/tests/topotests/bgp_set_aspath_exclude/__init__.py b/tests/topotests/bgp_set_aspath_exclude/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/topotests/bgp_set_aspath_exclude/r1/bgpd.conf b/tests/topotests/bgp_set_aspath_exclude/r1/bgpd.conf new file mode 100644 index 0000000000..9bef24f931 --- /dev/null +++ b/tests/topotests/bgp_set_aspath_exclude/r1/bgpd.conf @@ -0,0 +1,17 @@ +! +router bgp 65001 + no bgp ebgp-requires-policy + neighbor 192.168.1.2 remote-as external + neighbor 192.168.1.2 timers 3 10 + address-family ipv4 unicast + neighbor 192.168.1.2 route-map r2 in + exit-address-family +! +ip prefix-list p1 seq 5 permit 172.16.255.31/32 +! +route-map r2 permit 10 + match ip address prefix-list p1 + set as-path exclude 65003 +route-map r2 permit 20 + set as-path exclude all +! diff --git a/tests/topotests/bgp_set_aspath_exclude/r1/zebra.conf b/tests/topotests/bgp_set_aspath_exclude/r1/zebra.conf new file mode 100644 index 0000000000..acf120b200 --- /dev/null +++ b/tests/topotests/bgp_set_aspath_exclude/r1/zebra.conf @@ -0,0 +1,6 @@ +! +interface r1-eth0 + ip address 192.168.1.1/24 +! +ip forwarding +! diff --git a/tests/topotests/bgp_set_aspath_exclude/r2/bgpd.conf b/tests/topotests/bgp_set_aspath_exclude/r2/bgpd.conf new file mode 100644 index 0000000000..23367f94ff --- /dev/null +++ b/tests/topotests/bgp_set_aspath_exclude/r2/bgpd.conf @@ -0,0 +1,8 @@ +! +router bgp 65002 + no bgp ebgp-requires-policy + neighbor 192.168.1.1 remote-as external + neighbor 192.168.1.1 timers 3 10 + neighbor 192.168.2.1 remote-as external + neighbor 192.168.2.1 timers 3 10 +! diff --git a/tests/topotests/bgp_set_aspath_exclude/r2/zebra.conf b/tests/topotests/bgp_set_aspath_exclude/r2/zebra.conf new file mode 100644 index 0000000000..f229954341 --- /dev/null +++ b/tests/topotests/bgp_set_aspath_exclude/r2/zebra.conf @@ -0,0 +1,9 @@ +! +interface r2-eth0 + ip address 192.168.1.2/24 +! +interface r2-eth1 + ip address 192.168.2.2/24 +! +ip forwarding +! diff --git a/tests/topotests/bgp_set_aspath_exclude/r3/bgpd.conf b/tests/topotests/bgp_set_aspath_exclude/r3/bgpd.conf new file mode 100644 index 0000000000..b7a7ceda13 --- /dev/null +++ b/tests/topotests/bgp_set_aspath_exclude/r3/bgpd.conf @@ -0,0 +1,9 @@ +! +router bgp 65003 + no bgp ebgp-requires-policy + neighbor 192.168.2.2 remote-as external + neighbor 192.168.2.2 timers 3 10 + address-family ipv4 unicast + redistribute connected + exit-address-family +! diff --git a/tests/topotests/bgp_set_aspath_exclude/r3/zebra.conf b/tests/topotests/bgp_set_aspath_exclude/r3/zebra.conf new file mode 100644 index 0000000000..3fa6c64484 --- /dev/null +++ b/tests/topotests/bgp_set_aspath_exclude/r3/zebra.conf @@ -0,0 +1,10 @@ +! +int lo + ip address 172.16.255.31/32 + ip address 172.16.255.32/32 +! +interface r3-eth0 + ip address 192.168.2.1/24 +! +ip forwarding +! diff --git a/tests/topotests/bgp_set_aspath_exclude/test_bgp_set_aspath_exclude.py b/tests/topotests/bgp_set_aspath_exclude/test_bgp_set_aspath_exclude.py new file mode 100644 index 0000000000..8af7e7d60d --- /dev/null +++ b/tests/topotests/bgp_set_aspath_exclude/test_bgp_set_aspath_exclude.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: ISC +# +# test_bgp_set_aspath_exclude.py +# +# Copyright 2023 by 6WIND S.A. +# + +""" +Test if `set as-path exclude` is working correctly for route-maps. +""" + +import os +import sys +import json +import pytest +import functools + +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../")) + +# pylint: disable=C0413 +from lib import topotest +from lib.topogen import Topogen, TopoRouter, get_topogen + + +pytestmark = [pytest.mark.bgpd] + + +def build_topo(tgen): + for routern in range(1, 5): + tgen.add_router("r{}".format(routern)) + + switch = tgen.add_switch("s1") + switch.add_link(tgen.gears["r1"]) + switch.add_link(tgen.gears["r2"]) + + switch = tgen.add_switch("s2") + switch.add_link(tgen.gears["r2"]) + switch.add_link(tgen.gears["r3"]) + + +def setup_module(mod): + tgen = Topogen(build_topo, mod.__name__) + tgen.start_topology() + + router_list = tgen.routers() + + for i, (rname, router) in enumerate(router_list.items(), 1): + router.load_config( + TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname)) + ) + router.load_config( + TopoRouter.RD_BGP, os.path.join(CWD, "{}/bgpd.conf".format(rname)) + ) + + tgen.start_router() + + +def teardown_module(mod): + tgen = get_topogen() + tgen.stop_topology() + + +def test_bgp_set_aspath_exclude(): + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + def _bgp_converge(router): + output = json.loads(router.vtysh_cmd("show bgp ipv4 unicast json")) + expected = { + "routes": { + "172.16.255.31/32": [{"path": "65002"}], + "172.16.255.32/32": [{"path": ""}], + } + } + return topotest.json_cmp(output, expected) + + test_func = functools.partial(_bgp_converge, 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 route-map" + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args))