From 0ccad8a2b0b744d7ed64f53ab6e70a8d6aba69e6 Mon Sep 17 00:00:00 2001 From: Acee Lindem Date: Wed, 17 Apr 2024 20:14:56 +0000 Subject: [PATCH] ospfd: Add prefix-list filtering of OSPF neighbors on OSPF interface This commit adds the capabiity to filter OSPF neighbors using a prefix-list with rules matching the neighbor's IP source address. Configuration, filtering, immediate neighbor pruning, topo-tests, and documentation are included. The command is: ip ospf neighbor-filter [A.B.C.D] Signed-off-by: Acee Lindem --- doc/user/ospfd.rst | 26 +++ ospfd/ospf_interface.c | 34 +++- ospfd/ospf_interface.h | 7 + ospfd/ospf_packet.c | 15 ++ ospfd/ospf_vty.c | 89 ++++++++++ ospfd/ospf_zebra.c | 14 ++ .../ospf_p2mp/test_ospf_p2mp_broadcast.py | 152 ++++++++++++++++-- 7 files changed, 323 insertions(+), 14 deletions(-) diff --git a/doc/user/ospfd.rst b/doc/user/ospfd.rst index 47f8fad17b..5652e34a43 100644 --- a/doc/user/ospfd.rst +++ b/doc/user/ospfd.rst @@ -757,6 +757,32 @@ Interfaces optional IPv4 address is specified, the prefix suppression will apply to the OSPF interface associated with the specified interface address. +.. clicmd:: ip ospf neighbor-filter NAME [A.B.C.D] + + Configure an IP prefix-list to use to filter packets received from + OSPF neighbors on the OSPF interface. The prefix-list should include rules + to permit or deny OSPF neighbors by IP source address. This is useful for + multi-access interfaces where adjacencies with only a subset of the + reachable neighbors are desired. Applications include testing partially + meshed topologies, OSPF Denial of Sevice (DoS) mitigation, and avoidance + of adjacencies with OSPF neighbors not meeting traffic engineering criteria. + + Example: + +.. code-block:: frr + + ! + ! Prefix-list to block neighbor with source address 10.1.0.2 + ! + ip prefix-list nbr-filter seq 10 deny 10.1.0.2/32 + ip prefix-list nbr-filter seq 200 permit any + ! + ! Configure the neighbor filter prefix-list on interface eth0 + ! + interface eth0 + ip ospf neighbor-filter nbr-filter + ! + .. clicmd:: ip ospf area (A.B.C.D|(0-4294967295)) diff --git a/ospfd/ospf_interface.c b/ospfd/ospf_interface.c index 319db1efe2..0b27501019 100644 --- a/ospfd/ospf_interface.c +++ b/ospfd/ospf_interface.c @@ -19,6 +19,7 @@ #include "zclient.h" #include "bfd.h" #include "ldp_sync.h" +#include "plist.h" #include "ospfd/ospfd.h" #include "ospfd/ospf_bfd.h" @@ -67,6 +68,34 @@ int ospf_interface_neighbor_count(struct ospf_interface *oi) return count; } + +void ospf_intf_neighbor_filter_apply(struct ospf_interface *oi) +{ + struct route_node *rn; + struct ospf_neighbor *nbr = NULL; + struct prefix nbr_src_prefix = { AF_INET, IPV4_MAX_BITLEN, { 0 } }; + + if (!oi->nbr_filter) + return; + + /* + * Kill neighbors that don't match the neighbor filter prefix-list + * excluding the neighbor for the router itself and any neighbors + * that are already down. + */ + for (rn = route_top(oi->nbrs); rn; rn = route_next(rn)) { + nbr = rn->info; + if (nbr && nbr != oi->nbr_self && nbr->state != NSM_Down) { + nbr_src_prefix.u.prefix4 = nbr->src; + if (prefix_list_apply(oi->nbr_filter, + (struct prefix *)&( + nbr_src_prefix)) != + PREFIX_PERMIT) + OSPF_NSM_EVENT_EXECUTE(nbr, NSM_KillNbr); + } + } +} + int ospf_if_get_output_cost(struct ospf_interface *oi) { /* If all else fails, use default OSPF cost */ @@ -526,6 +555,7 @@ static struct ospf_if_params *ospf_new_if_params(void) UNSET_IF_PARAM(oip, if_area); UNSET_IF_PARAM(oip, opaque_capable); UNSET_IF_PARAM(oip, keychain_name); + UNSET_IF_PARAM(oip, nbr_filter_name); oip->auth_crypt = list_new(); @@ -544,6 +574,7 @@ static void ospf_del_if_params(struct interface *ifp, { list_delete(&oip->auth_crypt); XFREE(MTYPE_OSPF_IF_PARAMS, oip->keychain_name); + XFREE(MTYPE_OSPF_IF_PARAMS, oip->nbr_filter_name); ospf_interface_disable_bfd(ifp, oip); ldp_sync_info_free(&(oip->ldp_sync_info)); XFREE(MTYPE_OSPF_IF_PARAMS, oip); @@ -579,7 +610,8 @@ void ospf_free_if_params(struct interface *ifp, struct in_addr addr) !OSPF_IF_PARAM_CONFIGURED(oip, if_area) && !OSPF_IF_PARAM_CONFIGURED(oip, opaque_capable) && !OSPF_IF_PARAM_CONFIGURED(oip, prefix_suppression) && - !OSPF_IF_PARAM_CONFIGURED(oip, keychain_name) && + !OSPF_IF_PARAM_CONFIGURED(oip, keychain_name) && + !OSPF_IF_PARAM_CONFIGURED(oip, nbr_filter_name) && listcount(oip->auth_crypt) == 0) { ospf_del_if_params(ifp, oip); rn->info = NULL; diff --git a/ospfd/ospf_interface.h b/ospfd/ospf_interface.h index 721ab1a9d7..2e53fbfbfa 100644 --- a/ospfd/ospf_interface.h +++ b/ospfd/ospf_interface.h @@ -124,6 +124,9 @@ struct ospf_if_params { /* Opaque LSA capability at interface level (see RFC5250) */ DECLARE_IF_PARAM(bool, opaque_capable); + + /* Name of prefix-list name for packet source address filtering. */ + DECLARE_IF_PARAM(char *, nbr_filter_name); }; enum { MEMBER_ALLROUTERS = 0, @@ -242,6 +245,9 @@ struct ospf_interface { /* List of configured NBMA neighbor. */ struct list *nbr_nbma; + /* Configured prefix-list for filtering neighbors. */ + struct prefix_list *nbr_filter; + /* Graceful-Restart data. */ struct { struct { @@ -367,6 +373,7 @@ extern void ospf_crypt_key_add(struct list *list, struct crypt_key *key); extern int ospf_crypt_key_delete(struct list *list, uint8_t key_id); extern uint8_t ospf_default_iftype(struct interface *ifp); extern int ospf_interface_neighbor_count(struct ospf_interface *oi); +extern void ospf_intf_neighbor_filter_apply(struct ospf_interface *oi); /* Set all multicast memberships appropriately based on the type and state of the interface. */ diff --git a/ospfd/ospf_packet.c b/ospfd/ospf_packet.c index 60479ddcd1..87aaccad92 100644 --- a/ospfd/ospf_packet.c +++ b/ospfd/ospf_packet.c @@ -23,6 +23,7 @@ #endif #include "vrf.h" #include "lib_errors.h" +#include "plist.h" #include "ospfd/ospfd.h" #include "ospfd/ospf_network.h" @@ -2746,6 +2747,20 @@ static enum ospf_read_return_enum ospf_read_helper(struct ospf *ospf) /* associate packet with ospf interface */ oi = ospf_if_lookup_recv_if(ospf, iph->ip_src, ifp); + /* + * If a neighbor filter prefix-list is configured, apply it to the IP + * source address and ignore the packet if it doesn't match. + */ + if (oi && oi->nbr_filter) { + struct prefix ip_src_prefix = { AF_INET, IPV4_MAX_BITLEN, { 0 } }; + + ip_src_prefix.u.prefix4 = iph->ip_src; + if (prefix_list_apply(oi->nbr_filter, + (struct prefix *)&(ip_src_prefix)) != + PREFIX_PERMIT) + return OSPF_READ_CONTINUE; + } + /* * ospf_verify_header() relies on a valid "oi" and thus can be called * only after the passive/backbone/other checks below are passed. diff --git a/ospfd/ospf_vty.c b/ospfd/ospf_vty.c index b366b3ca30..ea483f72f5 100644 --- a/ospfd/ospf_vty.c +++ b/ospfd/ospf_vty.c @@ -4084,6 +4084,31 @@ static void show_ip_ospf_interface_sub(struct vty *vty, struct ospf *ospf, if (use_json) json_object_object_addf(json_ois, json_oi, "%pI4", &oi->address->u.prefix4); + + if (oi->nbr_filter) { + if (use_json) { + json_object_string_add(json_interface_sub, + "nbrFilterPrefixList", + prefix_list_name( + oi->nbr_filter)); + json_object_string_add(json_oi, + "nbrFilterPrefixList", + prefix_list_name( + oi->nbr_filter)); + } else + vty_out(vty, + " Neighbor filter prefix-list: %s\n", + prefix_list_name(oi->nbr_filter)); + } else { + if (use_json) { + json_object_string_add(json_interface_sub, + "nbrFilterPrefixList", + "N/A"); + json_object_string_add(json_oi, + "nbrFilterPrefixList", + "N/A"); + } + } } } @@ -9936,6 +9961,58 @@ DEFPY(ip_ospf_prefix_suppression, ip_ospf_prefix_suppression_addr_cmd, return CMD_SUCCESS; } +DEFPY(ip_ospf_neighbor_filter, ip_ospf_neighbor_filter_addr_cmd, + "[no] ip ospf neighbor-filter ![PREFIXLIST4_NAME]$prefix_list [A.B.C.D]$ip_addr", NO_STR + "IP Information\n" + "OSPF interface commands\n" + "Filter OSPF neighbor packets\n" + "Prefix-List used for filtering\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + struct ospf_if_params *params; + struct prefix_list *nbr_filter = NULL; + struct route_node *rn; + + params = IF_DEF_PARAMS(ifp); + + if (ip_addr.s_addr != INADDR_ANY) { + params = ospf_get_if_params(ifp, ip_addr); + ospf_if_update_params(ifp, ip_addr); + } + + if (params->nbr_filter_name) + XFREE(MTYPE_OSPF_IF_PARAMS, params->nbr_filter_name); + + if (no) { + UNSET_IF_PARAM(params, nbr_filter_name); + params->nbr_filter_name = NULL; + } else { + SET_IF_PARAM(params, nbr_filter_name); + params->nbr_filter_name = XSTRDUP(MTYPE_OSPF_IF_PARAMS, + prefix_list); + nbr_filter = prefix_list_lookup(AFI_IP, params->nbr_filter_name); + } + + /* + * Determine if there is a change in neighbor filter prefix-list for the + * interface. + */ + for (rn = route_top(IF_OIFS(ifp)); rn; rn = route_next(rn)) { + struct ospf_interface *oi = rn->info; + + if (oi && + (ip_addr.s_addr == INADDR_ANY || + IPV4_ADDR_SAME(&oi->address->u.prefix4, &ip_addr)) && + oi->nbr_filter != nbr_filter) { + oi->nbr_filter = nbr_filter; + if (oi->nbr_filter) + ospf_intf_neighbor_filter_apply(oi); + } + } + return CMD_SUCCESS; +} + DEFUN (ospf_max_metric_router_lsa_admin, ospf_max_metric_router_lsa_admin_cmd, "max-metric router-lsa administrative", @@ -12359,6 +12436,15 @@ static int config_write_interface_one(struct vty *vty, struct vrf *vrf) vty_out(vty, "\n"); } + /* neighbor-filter print. */ + if (OSPF_IF_PARAM_CONFIGURED(params, nbr_filter_name)) { + vty_out(vty, " ip ospf neighbor-filter %s", + params->nbr_filter_name); + if (params != IF_DEF_PARAMS(ifp) && rn) + vty_out(vty, " %pI4", &rn->p.u.prefix4); + vty_out(vty, "\n"); + } + while (1) { if (rn == NULL) rn = route_top(IF_OIFS_PARAMS(ifp)); @@ -13175,6 +13261,9 @@ static void ospf_vty_if_init(void) /* "ip ospf prefix-suppression" commands. */ install_element(INTERFACE_NODE, &ip_ospf_prefix_suppression_addr_cmd); + /* "ip ospf neighbor-filter" commands. */ + install_element(INTERFACE_NODE, &ip_ospf_neighbor_filter_addr_cmd); + /* These commands are compatibitliy for previous version. */ install_element(INTERFACE_NODE, &ospf_authentication_key_cmd); install_element(INTERFACE_NODE, &ospf_message_digest_key_cmd); diff --git a/ospfd/ospf_zebra.c b/ospfd/ospf_zebra.c index bb6cc3a89c..2c518f2c9e 100644 --- a/ospfd/ospf_zebra.c +++ b/ospfd/ospf_zebra.c @@ -1769,6 +1769,7 @@ static void ospf_prefix_list_update(struct prefix_list *plist) int type; int abr_inv = 0; struct ospf_area *area; + struct ospf_interface *oi; struct listnode *node, *n1; /* If OSPF instatnce does not exist, return right now. */ @@ -1824,6 +1825,19 @@ static void ospf_prefix_list_update(struct prefix_list *plist) } } + /* Update interface neighbor-filter lists. */ + for (ALL_LIST_ELEMENTS_RO(ospf->oiflist, node, oi)) { + if (OSPF_IF_PARAM(oi, nbr_filter_name) && + strcmp(OSPF_IF_PARAM(oi, nbr_filter_name), + prefix_list_name(plist)) == 0) { + oi->nbr_filter = prefix_list_lookup( + AFI_IP, + OSPF_IF_PARAM(oi, nbr_filter_name)); + if (oi->nbr_filter) + ospf_intf_neighbor_filter_apply(oi); + } + } + /* Schedule ABR task. */ if (IS_OSPF_ABR(ospf) && abr_inv) ospf_schedule_abr_task(ospf); diff --git a/tests/topotests/ospf_p2mp/test_ospf_p2mp_broadcast.py b/tests/topotests/ospf_p2mp/test_ospf_p2mp_broadcast.py index 352180b7ce..1f0f87959a 100644 --- a/tests/topotests/ospf_p2mp/test_ospf_p2mp_broadcast.py +++ b/tests/topotests/ospf_p2mp/test_ospf_p2mp_broadcast.py @@ -118,17 +118,21 @@ def teardown_module(mod): tgen.stop_topology() -def verify_p2mp_interface(tgen): +def verify_p2mp_interface(tgen, router, nbr_cnt, nbr_adj_cnt, nbr_filter): "Verify the P2MP Configuration and interface settings" - r1 = tgen.gears["r1"] + topo_router = tgen.gears[router] step("Test running configuration for P2MP configuration") rc = 0 - rc, _, _ = tgen.net["r1"].cmd_status( + rc, _, _ = tgen.net[router].cmd_status( "show running ospfd | grep 'ip ospf network point-to-multipoint'", warn=False ) - assertmsg = "'ip ospf network point-to-multipoint' applied, but not present in r1 configuration" + assertmsg = ( + "'ip ospf network point-to-multipoint' applied, but not present in " + + router + + "configuration" + ) assert rc, assertmsg step("Test OSPF interface for P2MP settings") @@ -145,11 +149,11 @@ def verify_p2mp_interface(tgen): "networkType": "POINTOMULTIPOINT", "cost": 10, "state": "Point-To-Point", - "nbrCount": 3, - "nbrAdjacentCount": 3, + "nbrCount": nbr_cnt, + "nbrAdjacentCount": nbr_adj_cnt, "prefixSuppression": False, "p2mpDelayReflood": False, - "p2mpNonBroadcast": False, + "nbrFilterPrefixList": nbr_filter, } }, "ipAddress": "10.1.0.1", @@ -161,16 +165,19 @@ def verify_p2mp_interface(tgen): "cost": 10, "state": "Point-To-Point", "opaqueCapable": True, - "nbrCount": 3, - "nbrAdjacentCount": 3, + "nbrCount": nbr_cnt, + "nbrAdjacentCount": nbr_adj_cnt, "prefixSuppression": False, "p2mpDelayReflood": False, - "p2mpNonBroadcast": False, + "nbrFilterPrefixList": nbr_filter, } } } test_func = partial( - topotest.router_json_cmp, r1, "show ip ospf interface r1-eth0 json", input_dict + topotest.router_json_cmp, + topo_router, + "show ip ospf interface r1-eth0 json", + input_dict, ) _, result = topotest.run_and_expect(test_func, None, count=60, wait=1) assertmsg = "P2MP Interface Mismatch on router r1" @@ -251,6 +258,23 @@ def verify_p2mp_neighbor(tgen, router, neighbor, state, intf_addr, interface): assert result is None, assertmsg +def verify_p2mp_neighbor_missing(tgen, router, neighbor): + topo_router = tgen.gears[router] + + step("Verify neighbor " + neighbor + " missing") + input_dict = {"default": {}} + test_func = partial( + topotest.router_json_cmp, + topo_router, + "show ip ospf neighbor " + neighbor + " json", + input_dict, + True, # Require exact match for missing neighbor + ) + _, result = topotest.run_and_expect(test_func, None, count=60, wait=1) + assertmsg = "P2MP Neighbor " + neighbor + " not missing" + assert result is None, assertmsg + + def verify_p2mp_route(tgen, router, prefix, prefix_len, nexthop, interface): topo_router = tgen.gears[router] @@ -288,7 +312,7 @@ def test_p2mp_broadcast_interface(): pytest.skip("Skipped because of router(s) failure") step("Verify router r1 interface r1-eth0 p2mp configuration") - verify_p2mp_interface(tgen) + verify_p2mp_interface(tgen, "r1", 3, 3, "N/A") step("Verify router r1 p2mp interface r1-eth0 neighbors") verify_p2mp_neighbor( @@ -313,7 +337,7 @@ def test_p2mp_broadcast_interface(): step("Verify router r1 interface r1-eth0 p2mp configuration application") r1.vtysh_cmd("conf t\ninterface r1-eth0\nip ospf network point-to-multipoint") - verify_p2mp_interface(tgen) + verify_p2mp_interface(tgen, "r1", 3, 3, "N/A") step("Verify restablishment of r1-eth0 p2mp neighbors") verify_p2mp_neighbor( @@ -332,6 +356,108 @@ def test_p2mp_broadcast_interface(): verify_p2mp_route(tgen, "r1", "10.1.4.0/24", 24, "10.1.0.4", "r1-eth0") +def test_p2mp_broadcast_neighbor_filter(): + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip("Skipped because of router(s) failure") + + step("Verify router r1 interface r1-eth0 p2mp configuration") + verify_p2mp_interface(tgen, "r1", 3, 3, "N/A") + + step("Verify router r1 p2mp interface r1-eth0 neighbors") + verify_p2mp_neighbor( + tgen, "r1", "2.2.2.2", "Full/DROther", "10.1.0.2", "r1-eth0:10.1.0.1" + ) + verify_p2mp_neighbor( + tgen, "r1", "3.3.3.3", "Full/DROther", "10.1.0.3", "r1-eth0:10.1.0.1" + ) + verify_p2mp_neighbor( + tgen, "r1", "4.4.4.4", "Full/DROther", "10.1.0.4", "r1-eth0:10.1.0.1" + ) + + step("Add OSPF interface neighbor-filter to r1") + r1 = tgen.gears["r1"] + r1.vtysh_cmd("conf t\ninterface r1-eth0\nip ospf neighbor-filter nbr-filter") + + step("Verify the R1 configuration of 'ip ospf neighbor-filter nbr-filter'") + neighbor_filter_cfg = ( + tgen.net["r1"] + .cmd( + 'vtysh -c "show running ospfd" | grep "^ ip ospf neighbor-filter nbr-filter"' + ) + .rstrip() + ) + assertmsg = ( + "'ip ospf neighbor-filter nbr-filter' applied, but not present in configuration" + ) + assert neighbor_filter_cfg == " ip ospf neighbor-filter nbr-filter", assertmsg + + step("Verify non-existent neighbor-filter is not applied to r1 interfaces") + verify_p2mp_interface(tgen, "r1", 3, 3, "N/A") + + step("Add nbr-filter prefix-list configuration to r1") + r1.vtysh_cmd("conf t\nip prefix-list nbr-filter seq 200 permit any") + + step( + "Verify neighbor-filter is now applied to r1 interface and neighbors still adjacent" + ) + verify_p2mp_interface(tgen, "r1", 3, 3, "nbr-filter") + + step("Add nbr-filter prefix-list configuration to block r4") + r1.vtysh_cmd("conf t\nip prefix-list nbr-filter seq 10 deny 10.1.0.4/32") + + step( + "Verify neighbor-filter is now applied to r1 interface and r4 is no longer adjacent" + ) + verify_p2mp_interface(tgen, "r1", 2, 2, "nbr-filter") + verify_p2mp_neighbor_missing(tgen, "r1", "4.4.4.4") + + step("Verify route to r4 subnet is now through r2") + verify_p2mp_route(tgen, "r1", "10.1.4.0/24", 24, "10.1.0.2", "r1-eth0") + + step("Add nbr-filter prefix-list configuration to block r2") + r1.vtysh_cmd("conf t\nip prefix-list nbr-filter seq 20 deny 10.1.0.2/32") + + step( + "Verify neighbor-filter is now applied to r1 interface and r2 is no longer adjacent" + ) + verify_p2mp_interface(tgen, "r1", 1, 1, "nbr-filter") + verify_p2mp_neighbor_missing(tgen, "r1", "2.2.2.2") + + step("Verify route to r4 and r2 subnet are now through r3") + verify_p2mp_route(tgen, "r1", "10.1.2.0/24", 24, "10.1.0.3", "r1-eth0") + verify_p2mp_route(tgen, "r1", "10.1.4.0/24", 24, "10.1.0.3", "r1-eth0") + + step("Remove neighbor filter configuration and verify") + r1.vtysh_cmd("conf t\ninterface r1-eth0\nno ip ospf neighbor-filter") + rc, _, _ = tgen.net["r1"].cmd_status( + "show running ospfd | grep -q 'ip ospf neighbor-filter'", warn=False + ) + assertmsg = "'ip ospf neighbor' not applied, but present in R1 configuration" + assert rc, assertmsg + + step("Verify interface neighbor-filter is removed and neighbors present") + verify_p2mp_interface(tgen, "r1", 3, 3, "N/A") + + step("Add neighbor filter configuration and verify neighbors are filtered") + r1.vtysh_cmd("conf t\ninterface r1-eth0\nip ospf neighbor-filter nbr-filter") + verify_p2mp_interface(tgen, "r1", 1, 1, "nbr-filter") + verify_p2mp_neighbor_missing(tgen, "r1", "2.2.2.2") + verify_p2mp_neighbor_missing(tgen, "r1", "4.4.4.4") + + step("Remove nbr-filter prefix-list configuration to block r2 and verify neighbor") + r1.vtysh_cmd("conf t\nno ip prefix-list nbr-filter seq 20") + verify_p2mp_interface(tgen, "r1", 2, 2, "nbr-filter") + verify_p2mp_neighbor( + tgen, "r1", "2.2.2.2", "Full/DROther", "10.1.0.2", "r1-eth0:10.1.0.1" + ) + + step("Delete nbr-filter prefix-list and verify neighbors are present") + r1.vtysh_cmd("conf t\nno ip prefix-list nbr-filter") + verify_p2mp_interface(tgen, "r1", 3, 3, "N/A") + + def test_memory_leak(): "Run the memory leak test and report results." tgen = get_topogen() -- 2.39.5