From a28af47280aac1476672584309f9de70db392836 Mon Sep 17 00:00:00 2001 From: Donatas Abraitis Date: Fri, 1 Jul 2022 23:23:14 +0300 Subject: [PATCH 1/6] doc: Add `allow-reserved-ranges` global command Signed-off-by: Donatas Abraitis --- doc/user/basic.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/user/basic.rst b/doc/user/basic.rst index 42faefd10b..7679a377eb 100644 --- a/doc/user/basic.rst +++ b/doc/user/basic.rst @@ -338,6 +338,12 @@ Basic Config Commands Restrict vty connections with an access list. +.. clicmd:: allow-reserved-ranges + + Allow using IPv4 reserved (Class E) IP ranges for daemons. E.g.: setting + IPv4 addresses for interfaces or allowing reserved ranges in BGP next-hops. + + Default: off. .. _sample-config-file: From ac156aecb5f292f565ccd0aeafade4cc0cad6028 Mon Sep 17 00:00:00 2001 From: Donatas Abraitis Date: Fri, 1 Jul 2022 23:24:52 +0300 Subject: [PATCH 2/6] lib, vtysh: Add `allow-reserved-ranges` global command It will be used to allow/deny using IPv4 reserved ranges (Class E) for Zebra (configuring interface address) or BGP (allow next-hop to be from this range). Signed-off-by: Donatas Abraitis --- lib/command.c | 26 ++++++++++++++++++++++++++ lib/command.h | 4 ++++ vtysh/vtysh.c | 17 +++++++++++++++++ vtysh/vtysh_config.c | 20 ++++++++++++-------- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/lib/command.c b/lib/command.c index cbecc81574..a23afb1e43 100644 --- a/lib/command.c +++ b/lib/command.c @@ -121,6 +121,11 @@ const char *cmd_version_get(void) return host.version; } +bool cmd_allow_reserved_ranges_get(void) +{ + return host.allow_reserved_ranges; +} + static int root_on_exit(struct vty *vty); /* Standard command node structures. */ @@ -454,6 +459,9 @@ static int config_write_host(struct vty *vty) if (name && name[0] != '\0') vty_out(vty, "domainname %s\n", name); + if (cmd_allow_reserved_ranges_get()) + vty_out(vty, "allow-reserved-ranges\n"); + /* The following are all configuration commands that are not sent to * watchfrr. For instance watchfrr is hardcoded to log to syslog so * we would always display 'log syslog informational' in the config @@ -2294,6 +2302,21 @@ DEFUN (no_banner_motd, return CMD_SUCCESS; } +DEFUN(allow_reserved_ranges, allow_reserved_ranges_cmd, "allow-reserved-ranges", + "Allow using IPv4 (Class E) reserved IP space\n") +{ + host.allow_reserved_ranges = true; + return CMD_SUCCESS; +} + +DEFUN(no_allow_reserved_ranges, no_allow_reserved_ranges_cmd, + "no allow-reserved-ranges", + NO_STR "Allow using IPv4 (Class E) reserved IP space\n") +{ + host.allow_reserved_ranges = false; + return CMD_SUCCESS; +} + int cmd_find_cmds(struct vty *vty, struct cmd_token **argv, int argc) { const struct cmd_node *node; @@ -2483,6 +2506,7 @@ void cmd_init(int terminal) host.lines = -1; cmd_banner_motd_line(FRR_DEFAULT_MOTD); host.motdfile = NULL; + host.allow_reserved_ranges = false; /* Install top nodes. */ install_node(&view_node); @@ -2552,6 +2576,8 @@ void cmd_init(int terminal) install_element(CONFIG_NODE, &no_banner_motd_cmd); install_element(CONFIG_NODE, &service_terminal_length_cmd); install_element(CONFIG_NODE, &no_service_terminal_length_cmd); + install_element(CONFIG_NODE, &allow_reserved_ranges_cmd); + install_element(CONFIG_NODE, &no_allow_reserved_ranges_cmd); log_cmd_init(); vrf_install_commands(); diff --git a/lib/command.h b/lib/command.h index 7363ed84c8..70e52708a7 100644 --- a/lib/command.h +++ b/lib/command.h @@ -84,6 +84,9 @@ struct host { /* Banner configuration. */ char *motd; char *motdfile; + + /* Allow using IPv4 (Class E) reserved IP space */ + bool allow_reserved_ranges; }; /* List of CLI nodes. Please remember to update the name array in command.c. */ @@ -614,6 +617,7 @@ extern const char *cmd_domainname_get(void); extern const char *cmd_system_get(void); extern const char *cmd_release_get(void); extern const char *cmd_version_get(void); +extern bool cmd_allow_reserved_ranges_get(void); /* NOT safe for general use; call this only if DEV_BUILD! */ extern void grammar_sandbox_init(void); diff --git a/vtysh/vtysh.c b/vtysh/vtysh.c index a52bd7b116..21bd2f4883 100644 --- a/vtysh/vtysh.c +++ b/vtysh/vtysh.c @@ -3140,6 +3140,20 @@ DEFUN(vtysh_debug_uid_backtrace, return err; } +DEFUNSH(VTYSH_ALL, vtysh_allow_reserved_ranges, vtysh_allow_reserved_ranges_cmd, + "allow-reserved-ranges", + "Allow using IPv4 (Class E) reserved IP space\n") +{ + return CMD_SUCCESS; +} + +DEFUNSH(VTYSH_ALL, no_vtysh_allow_reserved_ranges, + no_vtysh_allow_reserved_ranges_cmd, "no allow-reserved-ranges", + NO_STR "Allow using IPv4 (Class E) reserved IP space\n") +{ + return CMD_SUCCESS; +} + DEFUNSH(VTYSH_ALL, vtysh_service_password_encrypt, vtysh_service_password_encrypt_cmd, "service password-encryption", "Set up miscellaneous service\n" @@ -4902,6 +4916,9 @@ void vtysh_init_vty(void) install_element(CONFIG_NODE, &vtysh_service_password_encrypt_cmd); install_element(CONFIG_NODE, &no_vtysh_service_password_encrypt_cmd); + install_element(CONFIG_NODE, &vtysh_allow_reserved_ranges_cmd); + install_element(CONFIG_NODE, &no_vtysh_allow_reserved_ranges_cmd); + install_element(CONFIG_NODE, &vtysh_password_cmd); install_element(CONFIG_NODE, &no_vtysh_password_cmd); install_element(CONFIG_NODE, &vtysh_enable_password_cmd); diff --git a/vtysh/vtysh_config.c b/vtysh/vtysh_config.c index 3bd5489eef..a7ec2a93c2 100644 --- a/vtysh/vtysh_config.c +++ b/vtysh/vtysh_config.c @@ -478,14 +478,18 @@ void vtysh_config_parse_line(void *arg, const char *line) else if (strncmp(line, "rpki", strlen("rpki")) == 0) config = config_get(RPKI_NODE, line); else { - if (strncmp(line, "log", strlen("log")) == 0 - || strncmp(line, "hostname", strlen("hostname")) == 0 - || strncmp(line, "domainname", strlen("domainname")) == 0 - || strncmp(line, "frr", strlen("frr")) == 0 - || strncmp(line, "agentx", strlen("agentx")) == 0 - || strncmp(line, "no log", strlen("no log")) == 0 - || strncmp(line, "no ip prefix-list", strlen("no ip prefix-list")) == 0 - || strncmp(line, "no ipv6 prefix-list", strlen("no ipv6 prefix-list")) == 0) + if (strncmp(line, "log", strlen("log")) == 0 || + strncmp(line, "hostname", strlen("hostname")) == + 0 || + strncmp(line, "domainname", strlen("domainname")) == + 0 || + strncmp(line, "frr", strlen("frr")) == 0 || + strncmp(line, "agentx", strlen("agentx")) == 0 || + strncmp(line, "no log", strlen("no log")) == 0 || + strncmp(line, "no ip prefix-list", + strlen("no ip prefix-list")) == 0 || + strncmp(line, "no ipv6 prefix-list", + strlen("no ipv6 prefix-list")) == 0) config_add_line_uniq(config_top, line); else config_add_line(config_top, line); From d80132b13720857913e1e78bf61f25655062c488 Mon Sep 17 00:00:00 2001 From: Donatas Abraitis Date: Fri, 1 Jul 2022 23:26:24 +0300 Subject: [PATCH 3/6] lib: Allow using IPv4 (Class E) reserved block if enabled Signed-off-by: Donatas Abraitis --- lib/prefix.c | 18 ++++++++++++++++++ lib/prefix.h | 14 +++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/lib/prefix.c b/lib/prefix.c index 1a3efd32b1..e64b10bf24 100644 --- a/lib/prefix.c +++ b/lib/prefix.c @@ -21,6 +21,7 @@ #include +#include "command.h" #include "prefix.h" #include "ipaddr.h" #include "vty.h" @@ -1386,6 +1387,23 @@ char *evpn_es_df_alg2str(uint8_t df_alg, char *buf, int buf_len) return buf; } +bool ipv4_unicast_valid(const struct in_addr *addr) +{ + in_addr_t ip = ntohl(addr->s_addr); + + if (IPV4_CLASS_D(ip)) + return false; + + if (IPV4_CLASS_E(ip)) { + if (cmd_allow_reserved_ranges_get()) + return true; + else + return false; + } + + return true; +} + printfrr_ext_autoreg_p("EA", printfrr_ea); static ssize_t printfrr_ea(struct fbuf *buf, struct printfrr_eargs *ea, const void *ptr) diff --git a/lib/prefix.h b/lib/prefix.h index f9eef28a0b..6c51186f52 100644 --- a/lib/prefix.h +++ b/lib/prefix.h @@ -382,6 +382,8 @@ static inline void ipv4_addr_copy(struct in_addr *dst, #define IPV4_NET0(a) ((((uint32_t)(a)) & 0xff000000) == 0x00000000) #define IPV4_NET127(a) ((((uint32_t)(a)) & 0xff000000) == 0x7f000000) #define IPV4_LINKLOCAL(a) ((((uint32_t)(a)) & 0xffff0000) == 0xa9fe0000) +#define IPV4_CLASS_D(a) ((((uint32_t)(a)) & 0xf0000000) == 0xe0000000) +#define IPV4_CLASS_E(a) ((((uint32_t)(a)) & 0xf0000000) == 0xf0000000) #define IPV4_CLASS_DE(a) ((((uint32_t)(a)) & 0xe0000000) == 0xe0000000) #define IPV4_MC_LINKLOCAL(a) ((((uint32_t)(a)) & 0xffffff00) == 0xe0000000) @@ -507,17 +509,7 @@ extern int str_to_esi(const char *str, esi_t *esi); extern char *esi_to_str(const esi_t *esi, char *buf, int size); extern char *evpn_es_df_alg2str(uint8_t df_alg, char *buf, int buf_len); extern void prefix_evpn_hexdump(const struct prefix_evpn *p); - -static inline bool ipv4_unicast_valid(const struct in_addr *addr) -{ - - in_addr_t ip = ntohl(addr->s_addr); - - if (IPV4_CLASS_DE(ip)) - return false; - - return true; -} +extern bool ipv4_unicast_valid(const struct in_addr *addr); static inline int ipv6_martian(const struct in6_addr *addr) { From e66b8e39428674632533bd950ef32fbfffebe682 Mon Sep 17 00:00:00 2001 From: Donatas Abraitis Date: Fri, 1 Jul 2022 23:27:56 +0300 Subject: [PATCH 4/6] lib: Convert ipv4_martian to bool Signed-off-by: Donatas Abraitis --- lib/prefix.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/prefix.h b/lib/prefix.h index 6c51186f52..3a768572c4 100644 --- a/lib/prefix.h +++ b/lib/prefix.h @@ -526,14 +526,14 @@ static inline int ipv6_martian(const struct in6_addr *addr) extern int macstr2prefix_evpn(const char *str, struct prefix_evpn *p); /* NOTE: This routine expects the address argument in network byte order. */ -static inline int ipv4_martian(const struct in_addr *addr) +static inline bool ipv4_martian(const struct in_addr *addr) { in_addr_t ip = ntohl(addr->s_addr); if (IPV4_NET0(ip) || IPV4_NET127(ip) || !ipv4_unicast_valid(addr)) { - return 1; + return true; } - return 0; + return false; } static inline bool is_default_prefix4(const struct prefix_ipv4 *p) From 70632160e942d3777f799d9a705d9265d5981170 Mon Sep 17 00:00:00 2001 From: Donatas Abraitis Date: Fri, 1 Jul 2022 23:31:18 +0300 Subject: [PATCH 5/6] bgpd: Reuse ipv4_martian() when validating BGP next-hop Signed-off-by: Donatas Abraitis --- bgpd/bgp_attr.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bgpd/bgp_attr.c b/bgpd/bgp_attr.c index e7690ed8df..1a253b122a 100644 --- a/bgpd/bgp_attr.c +++ b/bgpd/bgp_attr.c @@ -1606,13 +1606,9 @@ static int bgp_attr_as4_path(struct bgp_attr_parser_args *args, enum bgp_attr_parse_ret bgp_attr_nexthop_valid(struct peer *peer, struct attr *attr) { - in_addr_t nexthop_h; struct bgp *bgp = peer->bgp; - nexthop_h = ntohl(attr->nexthop.s_addr); - if ((IPV4_NET0(nexthop_h) || IPV4_NET127(nexthop_h) || - !ipv4_unicast_valid(&attr->nexthop)) && - !bgp->allow_martian) { + if (ipv4_martian(&attr->nexthop) && !bgp->allow_martian) { uint8_t data[7]; /* type(2) + length(1) + nhop(4) */ char buf[INET_ADDRSTRLEN]; From bc7e6a17d41e3a9822934a701c6e95a008cddee0 Mon Sep 17 00:00:00 2001 From: Donatas Abraitis Date: Wed, 25 May 2022 22:20:53 +0300 Subject: [PATCH 6/6] tests: Check if we allow using IPv4 Class E for peering/next-hops Signed-off-by: Donatas Abraitis --- .../bgp_ipv4_class_e_peer/__init__.py | 0 .../bgp_ipv4_class_e_peer/r1/bgpd.conf | 12 ++ .../bgp_ipv4_class_e_peer/r1/zebra.conf | 11 ++ .../bgp_ipv4_class_e_peer/r2/bgpd.conf | 9 ++ .../bgp_ipv4_class_e_peer/r2/zebra.conf | 8 ++ .../test_bgp_ipv4_class_e_peer.py | 127 ++++++++++++++++++ 6 files changed, 167 insertions(+) create mode 100644 tests/topotests/bgp_ipv4_class_e_peer/__init__.py create mode 100644 tests/topotests/bgp_ipv4_class_e_peer/r1/bgpd.conf create mode 100644 tests/topotests/bgp_ipv4_class_e_peer/r1/zebra.conf create mode 100644 tests/topotests/bgp_ipv4_class_e_peer/r2/bgpd.conf create mode 100644 tests/topotests/bgp_ipv4_class_e_peer/r2/zebra.conf create mode 100644 tests/topotests/bgp_ipv4_class_e_peer/test_bgp_ipv4_class_e_peer.py diff --git a/tests/topotests/bgp_ipv4_class_e_peer/__init__.py b/tests/topotests/bgp_ipv4_class_e_peer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/topotests/bgp_ipv4_class_e_peer/r1/bgpd.conf b/tests/topotests/bgp_ipv4_class_e_peer/r1/bgpd.conf new file mode 100644 index 0000000000..bf0a68eea2 --- /dev/null +++ b/tests/topotests/bgp_ipv4_class_e_peer/r1/bgpd.conf @@ -0,0 +1,12 @@ +! +allow-reserved-ranges +! +router bgp 65001 + no bgp ebgp-requires-policy + neighbor 240.0.0.2 remote-as external + neighbor 240.0.0.2 timers 1 3 + neighbor 240.0.0.2 timers connect 1 + address-family ipv4 + redistribute connected + exit-address-family +! diff --git a/tests/topotests/bgp_ipv4_class_e_peer/r1/zebra.conf b/tests/topotests/bgp_ipv4_class_e_peer/r1/zebra.conf new file mode 100644 index 0000000000..d4ac46f248 --- /dev/null +++ b/tests/topotests/bgp_ipv4_class_e_peer/r1/zebra.conf @@ -0,0 +1,11 @@ +! +allow-reserved-ranges +! +interface lo + ip address 172.16.255.1/32 +! +interface r1-eth0 + ip address 240.0.0.1/24 +! +ip forwarding +! diff --git a/tests/topotests/bgp_ipv4_class_e_peer/r2/bgpd.conf b/tests/topotests/bgp_ipv4_class_e_peer/r2/bgpd.conf new file mode 100644 index 0000000000..7d08963633 --- /dev/null +++ b/tests/topotests/bgp_ipv4_class_e_peer/r2/bgpd.conf @@ -0,0 +1,9 @@ +! +allow-reserved-ranges +! +router bgp 65002 + no bgp ebgp-requires-policy + neighbor 240.0.0.1 remote-as external + neighbor 240.0.0.1 timers 1 3 + neighbor 240.0.0.1 timers connect 1 +! diff --git a/tests/topotests/bgp_ipv4_class_e_peer/r2/zebra.conf b/tests/topotests/bgp_ipv4_class_e_peer/r2/zebra.conf new file mode 100644 index 0000000000..f0a350fcea --- /dev/null +++ b/tests/topotests/bgp_ipv4_class_e_peer/r2/zebra.conf @@ -0,0 +1,8 @@ +! +allow-reserved-ranges +! +interface r2-eth0 + ip address 240.0.0.2/24 +! +ip forwarding +! diff --git a/tests/topotests/bgp_ipv4_class_e_peer/test_bgp_ipv4_class_e_peer.py b/tests/topotests/bgp_ipv4_class_e_peer/test_bgp_ipv4_class_e_peer.py new file mode 100644 index 0000000000..c2f0d2e0dc --- /dev/null +++ b/tests/topotests/bgp_ipv4_class_e_peer/test_bgp_ipv4_class_e_peer.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python + +# +# bgp_ipv4_class_e_peer.py +# +# Copyright (c) 2022 by +# Donatas Abraitis +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +""" +Check if the peering works by using IPv4 Class E IP ranges, and if +we don't treat next-hop as martian in such a case. +""" + +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 +from lib.common_config import step + +pytestmark = [pytest.mark.bgpd] + + +def build_topo(tgen): + for routern in range(1, 3): + tgen.add_router("r{}".format(routern)) + + switch = tgen.add_switch("s1") + switch.add_link(tgen.gears["r1"]) + switch.add_link(tgen.gears["r2"]) + + +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_ipv4_class_e_peer(): + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + router = tgen.gears["r2"] + + def _bgp_converge(): + output = json.loads(router.vtysh_cmd("show ip bgp neighbor 240.0.0.1 json")) + expected = { + "240.0.0.1": { + "bgpState": "Established", + "addressFamilyInfo": {"ipv4Unicast": {"acceptedPrefixCounter": 2}}, + } + } + return topotest.json_cmp(output, expected) + + def _bgp_next_hop_ipv4_class_e(): + output = json.loads( + router.vtysh_cmd("show bgp ipv4 unicast 172.16.255.1/32 json") + ) + expected = { + "paths": [ + { + "valid": True, + "nexthops": [ + { + "ip": "240.0.0.1", + "accessible": True, + } + ], + } + ] + } + return topotest.json_cmp(output, expected) + + step("Initial BGP converge") + test_func = functools.partial(_bgp_converge) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5) + assert result is None, "Failed to see BGP convergence on R2" + + step("Check if IPv4 BGP peering works with Class E IP ranges") + test_func = functools.partial(_bgp_next_hop_ipv4_class_e) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=0.5) + assert result is None, "Failed to see 172.16.255.1/32 via 240.0.0.1 on R2" + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args))