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 <prefix-list> [A.B.C.D]

Signed-off-by: Acee Lindem <acee@lindem.com>
This commit is contained in:
Acee Lindem 2024-04-17 20:14:56 +00:00
parent 84d1fb19e2
commit 0ccad8a2b0
7 changed files with 323 additions and 14 deletions

View file

@ -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))

View file

@ -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);
@ -580,6 +611,7 @@ void ospf_free_if_params(struct interface *ifp, struct in_addr addr)
!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, nbr_filter_name) &&
listcount(oip->auth_crypt) == 0) {
ospf_del_if_params(ifp, oip);
rn->info = NULL;

View file

@ -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. */

View file

@ -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.

View file

@ -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);

View file

@ -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);

View file

@ -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()