diff --git a/doc/user/pim.rst b/doc/user/pim.rst index c139e64880..9e4c7bb94a 100644 --- a/doc/user/pim.rst +++ b/doc/user/pim.rst @@ -437,6 +437,14 @@ is in a vrf, enter the interface command with the vrf keyword at the end. Set the IGMP version used on this interface. The default value is 3. +.. clicmd:: ip igmp max-groups (0-4294967295) + + Set the maximum number of IGMP groups that the can be joined on an interface. + +.. clicmd:: ip igmp max-sources (0-4294967295) + + Set the maximum number of IGMP sources to learn per group. + .. clicmd:: ip multicast boundary oil WORD Set a PIM multicast boundary, based upon the WORD prefix-list. If a PIM join diff --git a/doc/user/pimv6.rst b/doc/user/pimv6.rst index 555c830615..bd5430f51e 100644 --- a/doc/user/pimv6.rst +++ b/doc/user/pimv6.rst @@ -258,6 +258,14 @@ is in a vrf, enter the interface command with the vrf keyword at the end. Set the MLD version used on this interface. The default value is 2. +.. clicmd:: ipv6 mld max-groups (0-4294967295) + + Set the maximum number of MLD groups that the can be joined on an interface. + +.. clicmd:: ipv6 mld max-sources (0-4294967295) + + Set the maximum number of MLD sources to learn per group. + .. clicmd:: ipv6 multicast boundary oil WORD Set a PIMv6 multicast boundary, based upon the WORD prefix-list. If a PIMv6 @@ -481,6 +489,10 @@ PIMv6 Clear Commands Clear commands reset various variables. +.. clicmd:: clear ipv6 mld [vrf NAME] interfaces + + Reset learned multicast groups / sources. + .. clicmd:: clear ipv6 mroute Reset multicast routes. diff --git a/pimd/pim6_cmd.c b/pimd/pim6_cmd.c index 12493b7dbb..40bd7caf7d 100644 --- a/pimd/pim6_cmd.c +++ b/pimd/pim6_cmd.c @@ -1612,6 +1612,43 @@ DEFPY (interface_no_ipv6_mld_version, "frr-routing:ipv6"); } +DEFPY_YANG(interface_ipv6_mld_limits, + interface_ipv6_mld_limits_cmd, + "[no] ipv6 mld ", + NO_STR + IPV6_STR + IFACE_MLD_STR + "Limit number of MLDv2 sources to track\n" + "Permitted number of sources\n" + "Limit number of MLD group memberships to track\n" + "Permitted number of groups\n") +{ + const char *xpath; + + assert(do_src || do_grp); + if (do_src) + xpath = "./max-sources"; + else + xpath = "./max-groups"; + + if (no) + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, val_str); + + return nb_cli_apply_changes(vty, FRR_GMP_INTERFACE_XPATH, FRR_PIM_AF_XPATH_VAL); +} + +ALIAS_YANG(interface_ipv6_mld_limits, + no_interface_ipv6_mld_limits_cmd, + "no ipv6 mld ", + NO_STR + IPV6_STR + IFACE_MLD_STR + "Limit number of MLDv2 sources to track\n" + "Limit number of MLD group memberships to track\n") + DEFPY (interface_ipv6_mld_query_interval, interface_ipv6_mld_query_interval_cmd, "ipv6 mld query-interval (1-65535)$q_interval", @@ -2341,6 +2378,32 @@ DEFPY (show_ipv6_pim_bsrp, return pim_show_group_rp_mappings_info_helper(vrf, vty, !!json); } +DEFPY(clear_ipv6_mld_interfaces, + clear_ipv6_mld_interfaces_cmd, + "clear ipv6 mld [vrf NAME$vrf_name] interfaces", + CLEAR_STR + IPV6_STR + "MLD clear commands\n" + VRF_CMD_HELP_STR + "Reset MLD interfaces\n") +{ + struct interface *ifp; + struct vrf *vrf; + + vrf = vrf_name ? vrf_lookup_by_name(vrf_name) : vrf_lookup_by_id(VRF_DEFAULT); + if (!vrf) { + vty_out(vty, "Specified VRF: %s does not exist\n", vrf_name); + return CMD_WARNING; + } + + FOR_ALL_INTERFACES (vrf, ifp) + pim_if_addr_del_all(ifp); + FOR_ALL_INTERFACES (vrf, ifp) + pim_if_addr_add_all(ifp); + + return CMD_SUCCESS; +} + DEFPY (clear_ipv6_pim_statistics, clear_ipv6_pim_statistics_cmd, "clear ipv6 pim statistics [vrf NAME]$name", @@ -2865,6 +2928,9 @@ void pim_cmd_init(void) install_element(INTERFACE_NODE, &interface_no_ipv6_pim_boundary_oil_cmd); install_element(INTERFACE_NODE, &interface_ipv6_mroute_cmd); install_element(INTERFACE_NODE, &interface_no_ipv6_mroute_cmd); + install_element(INTERFACE_NODE, &interface_ipv6_mld_limits_cmd); + install_element(INTERFACE_NODE, &no_interface_ipv6_mld_limits_cmd); + /* Install BSM command */ install_element(INTERFACE_NODE, &ipv6_pim_bsm_cmd); install_element(INTERFACE_NODE, &no_ipv6_pim_bsm_cmd); @@ -2935,6 +3001,7 @@ void pim_cmd_init(void) install_element(VIEW_NODE, &show_ipv6_pim_bsr_cmd); install_element(VIEW_NODE, &show_ipv6_pim_bsm_db_cmd); install_element(VIEW_NODE, &show_ipv6_pim_bsrp_cmd); + install_element(ENABLE_NODE, &clear_ipv6_mld_interfaces_cmd); install_element(ENABLE_NODE, &clear_ipv6_pim_statistics_cmd); install_element(ENABLE_NODE, &clear_ipv6_mroute_cmd); install_element(ENABLE_NODE, &clear_ipv6_pim_oil_cmd); diff --git a/pimd/pim6_mld.c b/pimd/pim6_mld.c index acfb0c3af3..d7e0314d3b 100644 --- a/pimd/pim6_mld.c +++ b/pimd/pim6_mld.c @@ -190,11 +190,26 @@ static struct gm_sg *gm_sg_find(struct gm_if *gm_ifp, pim_addr grp, return gm_sgs_find(gm_ifp->sgs, &ref); } +static bool gm_sg_has_group(struct gm_sgs_head *sgs, const pim_addr group) +{ + struct gm_sg *sg; + + frr_each (gm_sgs, sgs, sg) + if (pim_addr_cmp(sg->sgaddr.grp, group) == 0) + return true; + + return false; +} + static struct gm_sg *gm_sg_make(struct gm_if *gm_ifp, pim_addr grp, pim_addr src) { struct gm_sg *ret, *prev; + /* Count all unique group members. */ + if (!gm_sg_has_group(gm_ifp->sgs, grp)) + gm_ifp->groups_count++; + ret = XCALLOC(MTYPE_GM_SG, sizeof(*ret)); ret->sgaddr.grp = grp; ret->sgaddr.src = src; @@ -212,6 +227,47 @@ static struct gm_sg *gm_sg_make(struct gm_if *gm_ifp, pim_addr grp, return ret; } +static size_t gm_sg_source_count(struct gm_sgs_head *sgs, const pim_addr group) +{ + struct gm_sg *sg; + size_t source_count; + + source_count = 0; + frr_each (gm_sgs, sgs, sg) + if (pim_addr_cmp(sg->sgaddr.grp, group) == 0) + source_count++; + + return source_count; +} + +static bool gm_sg_limit_reached(struct gm_if *gm_if, const pim_addr source, const pim_addr group) +{ + const struct pim_interface *pim_interface = gm_if->ifp->info; + + if (!gm_sg_has_group(gm_if->sgs, group)) { + if (gm_if->groups_count >= pim_interface->gm_group_limit) { + if (PIM_DEBUG_GM_TRACE) + zlog_debug("interface %s has reached group limit (%u), refusing to add group %pPA", + gm_if->ifp->name, pim_interface->gm_group_limit, &group); + + return true; + } + + return false; + } + + if (gm_sg_source_count(gm_if->sgs, group) >= pim_interface->gm_source_limit) { + if (PIM_DEBUG_GM_TRACE) { + zlog_debug("interface %s has reached source limit (%u), refusing to add source %pPA (group %pPA)", + gm_if->ifp->name, pim_interface->gm_source_limit, &source, + &group); + } + return true; + } + + return false; +} + /* * interface -> packets, sorted by expiry (because add_tail insert order) */ @@ -471,6 +527,11 @@ static void gm_sg_update(struct gm_sg *sg, bool has_expired) zlog_debug(log_sg(sg, "dropping")); gm_sgs_del(gm_ifp->sgs, sg); + + /* Decrement unique group members counter. */ + if (!gm_sg_has_group(gm_ifp->sgs, sg->sgaddr.grp)) + gm_ifp->groups_count--; + gm_sg_free(sg); } } @@ -634,8 +695,12 @@ static void gm_handle_v2_pass1(struct gm_packet_state *pkt, case MLD_RECTYPE_CHANGE_TO_EXCLUDE: /* this always replaces or creates state */ is_excl = true; - if (!grp) + if (!grp) { + if (gm_sg_limit_reached(pkt->iface, PIMADDR_ANY, rechdr->grp)) + return; + grp = gm_sg_make(pkt->iface, rechdr->grp, PIMADDR_ANY); + } item = gm_packet_sg_setup(pkt, grp, is_excl, false); item->n_exclude = n_src; @@ -700,9 +765,13 @@ static void gm_handle_v2_pass1(struct gm_packet_state *pkt, struct gm_sg *sg; sg = gm_sg_find(pkt->iface, rechdr->grp, rechdr->srcs[j]); - if (!sg) + if (!sg) { + if (gm_sg_limit_reached(pkt->iface, rechdr->srcs[j], rechdr->grp)) + return; + sg = gm_sg_make(pkt->iface, rechdr->grp, rechdr->srcs[j]); + } gm_packet_sg_setup(pkt, sg, is_excl, true); } @@ -952,6 +1021,10 @@ static void gm_handle_v1_report(struct gm_if *gm_ifp, hdr = (struct mld_v1_pkt *)data; + if (!gm_sg_has_group(gm_ifp->sgs, hdr->grp) && + gm_sg_limit_reached(gm_ifp, PIMADDR_ANY, hdr->grp)) + return; + max_entries = 1; pkt = XCALLOC(MTYPE_GM_STATE, offsetof(struct gm_packet_state, items[max_entries])); @@ -1255,6 +1328,9 @@ static void gm_handle_q_groupsrc(struct gm_if *gm_ifp, for (i = 0; i < n_src; i++) { sg = gm_sg_find(gm_ifp, grp, srcs[i]); + if (sg == NULL) + continue; + GM_UPDATE_SG_STATE(sg); gm_sg_timer_start(gm_ifp, sg, timers->expire_wait); } diff --git a/pimd/pim6_mld.h b/pimd/pim6_mld.h index 183ab2fc50..c5a9708961 100644 --- a/pimd/pim6_mld.h +++ b/pimd/pim6_mld.h @@ -350,6 +350,8 @@ struct gm_if { struct gm_subscribers_head subscribers[1]; struct gm_packet_expires_head expires[1]; + size_t groups_count; + struct timeval started; struct gm_if_stats stats; }; diff --git a/pimd/pim_cmd.c b/pimd/pim_cmd.c index a1ad261869..fa9c6f9537 100644 --- a/pimd/pim_cmd.c +++ b/pimd/pim_cmd.c @@ -5656,6 +5656,43 @@ DEFUN (interface_no_ip_igmp_last_member_query_interval, return gm_process_no_last_member_query_interval_cmd(vty); } +DEFPY_YANG(interface_ip_igmp_limits, + interface_ip_igmp_limits_cmd, + "[no] ip igmp ", + NO_STR + IP_STR + IFACE_IGMP_STR + "Limit number of IGMPv3 sources to track\n" + "Permitted number of sources\n" + "Limit number of IGMP group memberships to track\n" + "Permitted number of groups\n") +{ + const char *xpath; + + assert(do_src || do_grp); + if (do_src) + xpath = "./max-sources"; + else + xpath = "./max-groups"; + + if (no) + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, val_str); + + return nb_cli_apply_changes(vty, FRR_GMP_INTERFACE_XPATH, FRR_PIM_AF_XPATH_VAL); +} + +ALIAS_YANG(interface_ip_igmp_limits, + no_interface_ip_igmp_limits_cmd, + "no ip igmp ", + NO_STR + IP_STR + IFACE_IGMP_STR + "Limit number of IGMPv3 sources to track\n" + "Limit number of IGMP group memberships to track\n") + DEFUN (interface_ip_pim_drprio, interface_ip_pim_drprio_cmd, "ip pim drpriority (0-4294967295)", @@ -9101,6 +9138,8 @@ void pim_cmd_init(void) install_element(INTERFACE_NODE, &interface_no_ip_igmp_last_member_query_interval_cmd); install_element(INTERFACE_NODE, &interface_ip_igmp_proxy_cmd); + install_element(INTERFACE_NODE, &interface_ip_igmp_limits_cmd); + install_element(INTERFACE_NODE, &no_interface_ip_igmp_limits_cmd); install_element(INTERFACE_NODE, &interface_ip_pim_activeactive_cmd); install_element(INTERFACE_NODE, &interface_ip_pim_ssm_cmd); install_element(INTERFACE_NODE, &interface_no_ip_pim_ssm_cmd); diff --git a/pimd/pim_iface.c b/pimd/pim_iface.c index 9316cebc0a..8ec51ddc39 100644 --- a/pimd/pim_iface.c +++ b/pimd/pim_iface.c @@ -128,6 +128,8 @@ struct pim_interface *pim_if_new(struct interface *ifp, bool gm, bool pim, pim_ifp->gm_specific_query_max_response_time_dsec = GM_SPECIFIC_QUERY_MAX_RESPONSE_TIME_DSEC; pim_ifp->gm_last_member_query_count = GM_DEFAULT_ROBUSTNESS_VARIABLE; + pim_ifp->gm_group_limit = UINT32_MAX; + pim_ifp->gm_source_limit = UINT32_MAX; /* BSM config on interface: true by default */ pim_ifp->bsm_enable = true; diff --git a/pimd/pim_iface.h b/pimd/pim_iface.h index 90a81a21d0..0a7993fd27 100644 --- a/pimd/pim_iface.h +++ b/pimd/pim_iface.h @@ -105,6 +105,8 @@ struct pim_interface { struct gm_if *mld; + uint32_t gm_source_limit, gm_group_limit; + int pim_sock_fd; /* PIM socket file descriptor */ struct event *t_pim_sock_read; /* thread for reading PIM socket */ int64_t pim_sock_creation; /* timestamp of PIM socket creation */ diff --git a/pimd/pim_igmp.c b/pimd/pim_igmp.c index 12f424248f..b1b4566499 100644 --- a/pimd/pim_igmp.c +++ b/pimd/pim_igmp.c @@ -1416,6 +1416,14 @@ struct gm_group *igmp_add_group_by_addr(struct gm_sock *igmp, __func__, &group_addr); return NULL; } + + if (listcount(pim_ifp->gm_group_list) >= pim_ifp->gm_group_limit) { + if (PIM_DEBUG_GM_TRACE) + zlog_debug("interface %s has reached group limit (%u), refusing to add group %pI4", + igmp->interface->name, pim_ifp->gm_group_limit, &group_addr); + return NULL; + } + /* Non-existant group is created as INCLUDE {empty}: diff --git a/pimd/pim_igmpv3.c b/pimd/pim_igmpv3.c index 7348d8130f..7cb168dc5d 100644 --- a/pimd/pim_igmpv3.c +++ b/pimd/pim_igmpv3.c @@ -423,6 +423,7 @@ struct gm_source *igmp_find_source_by_addr(struct gm_group *group, struct gm_source *igmp_get_source_by_addr(struct gm_group *group, struct in_addr src_addr, bool *new) { + const struct pim_interface *pim_interface = group->interface->info; struct gm_source *src; if (new) @@ -432,6 +433,14 @@ struct gm_source *igmp_get_source_by_addr(struct gm_group *group, if (src) return src; + if (listcount(group->group_source_list) >= pim_interface->gm_source_limit) { + if (PIM_DEBUG_GM_TRACE) + zlog_debug("interface %s has reached source limit (%u), refusing to add source %pI4 (group %pI4)", + group->interface->name, pim_interface->gm_source_limit, + &src_addr, &group->group_addr); + return NULL; + } + if (PIM_DEBUG_GM_TRACE) { char group_str[INET_ADDRSTRLEN]; char source_str[INET_ADDRSTRLEN]; diff --git a/pimd/pim_nb.c b/pimd/pim_nb.c index ea9ce3cecb..62c5d531d9 100644 --- a/pimd/pim_nb.c +++ b/pimd/pim_nb.c @@ -724,6 +724,18 @@ const struct frr_yang_module_info frr_gmp_info = { .create = lib_interface_gmp_address_family_join_group_create, .destroy = lib_interface_gmp_address_family_join_group_destroy, } + }, + { + .xpath = "/frr-interface:lib/interface/frr-gmp:gmp/address-family/max-sources", + .cbs = { + .modify = lib_interface_gm_max_sources_modify, + } + }, + { + .xpath = "/frr-interface:lib/interface/frr-gmp:gmp/address-family/max-groups", + .cbs = { + .modify = lib_interface_gm_max_groups_modify, + } }, { .xpath = "/frr-interface:lib/interface/frr-gmp:gmp/address-family/proxy", diff --git a/pimd/pim_nb.h b/pimd/pim_nb.h index a15c6e6d9f..1656313fc2 100644 --- a/pimd/pim_nb.h +++ b/pimd/pim_nb.h @@ -287,6 +287,8 @@ int lib_interface_gmp_address_family_static_group_create( struct nb_cb_create_args *args); int lib_interface_gmp_address_family_static_group_destroy( struct nb_cb_destroy_args *args); +int lib_interface_gm_max_sources_modify(struct nb_cb_modify_args *args); +int lib_interface_gm_max_groups_modify(struct nb_cb_modify_args *args); /* * Callback registered with routing_nb lib to validate only diff --git a/pimd/pim_nb_config.c b/pimd/pim_nb_config.c index 5203f78b92..c926696610 100644 --- a/pimd/pim_nb_config.c +++ b/pimd/pim_nb_config.c @@ -4396,6 +4396,72 @@ int lib_interface_gmp_address_family_last_member_query_interval_modify( return NB_OK; } +/* + * XPath: /frr-interface:lib/interface/frr-gmp:gmp/address-family/max-groups + */ +int lib_interface_gm_max_groups_modify(struct nb_cb_modify_args *args) +{ + struct interface *ifp; + struct pim_interface *pim_ifp; + const char *ifp_name; + const struct lyd_node *if_dnode; + + switch (args->event) { + case NB_EV_VALIDATE: + if_dnode = yang_dnode_get_parent(args->dnode, "interface"); + if (!is_pim_interface(if_dnode)) { + ifp_name = yang_dnode_get_string(if_dnode, "name"); + snprintf(args->errmsg, args->errmsg_len, + "multicast not enabled on interface %s", ifp_name); + return NB_ERR_VALIDATION; + } + break; + case NB_EV_PREPARE: + case NB_EV_ABORT: + break; + case NB_EV_APPLY: + ifp = nb_running_get_entry(args->dnode, NULL, true); + pim_ifp = ifp->info; + pim_ifp->gm_group_limit = yang_dnode_get_uint32(args->dnode, NULL); + break; + } + + return NB_OK; +} + +/* + * XPath: /frr-interface:lib/interface/frr-gmp:gmp/address-family/max-sources + */ +int lib_interface_gm_max_sources_modify(struct nb_cb_modify_args *args) +{ + struct interface *ifp; + struct pim_interface *pim_ifp; + const char *ifp_name; + const struct lyd_node *if_dnode; + + switch (args->event) { + case NB_EV_VALIDATE: + if_dnode = yang_dnode_get_parent(args->dnode, "interface"); + if (!is_pim_interface(if_dnode)) { + ifp_name = yang_dnode_get_string(if_dnode, "name"); + snprintf(args->errmsg, args->errmsg_len, + "multicast not enabled on interface %s", ifp_name); + return NB_ERR_VALIDATION; + } + break; + case NB_EV_PREPARE: + case NB_EV_ABORT: + break; + case NB_EV_APPLY: + ifp = nb_running_get_entry(args->dnode, NULL, true); + pim_ifp = ifp->info; + pim_ifp->gm_source_limit = yang_dnode_get_uint32(args->dnode, NULL); + break; + } + + return NB_OK; +} + /* * XPath: /frr-interface:lib/interface/frr-gmp:gmp/address-family/robustness-variable */ diff --git a/pimd/pim_vty.c b/pimd/pim_vty.c index a972a38c72..64750a22f6 100644 --- a/pimd/pim_vty.c +++ b/pimd/pim_vty.c @@ -457,6 +457,20 @@ int pim_config_write(struct vty *vty, int writes, struct interface *ifp, ++writes; } + /* IF igmp/mld max-sources */ + if (pim_ifp->gm_source_limit != UINT32_MAX) { + vty_out(vty, " " PIM_AF_NAME " " GM_AF_DBG " max-sources %u\n", + pim_ifp->gm_source_limit); + ++writes; + } + + /* IF igmp/mld max-groups */ + if (pim_ifp->gm_group_limit != UINT32_MAX) { + vty_out(vty, " " PIM_AF_NAME " " GM_AF_DBG " max-groups %u\n", + pim_ifp->gm_group_limit); + ++writes; + } + /* IF ip pim drpriority */ if (pim_ifp->pim_dr_priority != PIM_DEFAULT_DR_PRIORITY) { vty_out(vty, " " PIM_AF_NAME " pim drpriority %u\n", diff --git a/tests/topotests/lib/mcast-tester.py b/tests/topotests/lib/mcast-tester.py index 5efbecd5e5..3645eef25e 100755 --- a/tests/topotests/lib/mcast-tester.py +++ b/tests/topotests/lib/mcast-tester.py @@ -34,16 +34,64 @@ def interface_name_to_index(name): return None -def multicast_join(sock, ifindex, group, port): +def interface_index_to_address(index, iptype="inet"): + "Gets the interface main address using its name. Returns None on failure." + interfaces = json.loads(subprocess.check_output("ip -j addr show", shell=True)) + + for interface in interfaces: + if interface["ifindex"] == index: + break + + for address in interface["addr_info"]: + if address["family"] == iptype: + break + + local_address = ipaddress.ip_address(address["local"]) + + return local_address.packed + + +def group_source_req(ifindex, group, source): + "Packs the information into 'struct group_source_req' format." + mreq = struct.pack(" 0: # Block to ensure packet send. msock.setblocking(True) else: - multicast_join(msock, ifindex, args.group, args.port) + multicast_join(msock, ifindex, args.group, args.port, args.source) def should_exit(): diff --git a/tests/topotests/multicast_features/__init__.py b/tests/topotests/multicast_features/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/topotests/multicast_features/r1/frr.conf b/tests/topotests/multicast_features/r1/frr.conf new file mode 100644 index 0000000000..bd1cc4103c --- /dev/null +++ b/tests/topotests/multicast_features/r1/frr.conf @@ -0,0 +1,48 @@ +log commands +! +interface r1-eth0 + ip address 192.168.1.1/24 + ip pim + ipv6 address 2001:db8:1::1/64 + ipv6 pim +! +interface r1-eth1 + ip address 192.168.2.1/24 + ip pim + ipv6 address 2001:db8:2::1/64 + ipv6 pim +! +interface r1-eth2 + ip address 192.168.100.1/24 + ip igmp + ip pim passive + ipv6 address 2001:db8:ffff::1/128 + ipv6 mld + ipv6 pim passive +! +interface lo + ip address 10.254.254.1/32 + ip pim + ip pim use-source 10.254.254.1 +! +router bgp 65100 + no bgp ebgp-requires-policy + no bgp network import-check + neighbor 192.168.1.2 remote-as 65200 + neighbor 192.168.2.2 remote-as 65300 + ! + address-family ipv4 unicast + redistribute connected + exit-address-family + address-family ipv6 unicast + redistribute connected + neighbor 192.168.1.2 activate + neighbor 192.168.2.2 activate + exit-address-family +! +router pim + rp 10.254.254.1 +! +router pim6 + rp 2001:db8:ffff::1 +! \ No newline at end of file diff --git a/tests/topotests/multicast_features/r2/frr.conf b/tests/topotests/multicast_features/r2/frr.conf new file mode 100644 index 0000000000..f647b93ca3 --- /dev/null +++ b/tests/topotests/multicast_features/r2/frr.conf @@ -0,0 +1,37 @@ +log commands +! +interface r2-eth0 + ip address 192.168.1.2/24 + ip pim + ipv6 address 2001:db8:1::2/64 + ipv6 pim +! +interface r2-eth1 + ip address 192.168.101.1/24 + ip igmp + ip pim passive +! +interface lo + ip address 10.254.254.2/32 + ipv6 address 2001:db8:ffff::2/128 + ipv6 pim passive +! +router bgp 65200 + no bgp ebgp-requires-policy + no bgp network import-check + neighbor 192.168.1.1 remote-as 65100 + ! + address-family ipv4 unicast + redistribute connected + exit-address-family + address-family ipv6 unicast + redistribute connected + neighbor 192.168.1.1 activate + exit-address-family +! +router pim + rp 10.254.254.2 +! +router pim6 + rp 2001:db8:ffff::2 +! \ No newline at end of file diff --git a/tests/topotests/multicast_features/r3/frr.conf b/tests/topotests/multicast_features/r3/frr.conf new file mode 100644 index 0000000000..2ac00c8798 --- /dev/null +++ b/tests/topotests/multicast_features/r3/frr.conf @@ -0,0 +1,34 @@ +log commands +! +interface r3-eth0 + ip address 192.168.2.2/24 + ip pim + ipv6 address 2001:db8:2::2/64 + ipv6 pim +! +interface lo + ip address 10.254.254.3/32 + ip pim + ip pim use-source 10.254.254.3 + ipv6 address 2001:db8:ffff::3/128 + ipv6 pim passive +! +router bgp 65300 + no bgp ebgp-requires-policy + no bgp network import-check + neighbor 192.168.2.1 remote-as 65100 + ! + address-family ipv4 unicast + redistribute connected + exit-address-family + address-family ipv6 unicast + redistribute connected + neighbor 192.168.2.1 activate + exit-address-family +! +router pim + rp 10.254.254.1 +! +router pim6 + rp 2001:db8:ffff::1 +! \ No newline at end of file diff --git a/tests/topotests/multicast_features/test_multicast_features.py b/tests/topotests/multicast_features/test_multicast_features.py new file mode 100644 index 0000000000..9c1f4af99f --- /dev/null +++ b/tests/topotests/multicast_features/test_multicast_features.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: ISC + +# +# test_multicast_features.py +# Part of NetDEF Topology Tests +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# + +""" +test_multicast_features.py: Test the FRR PIM multicast features. +""" + +import os +import sys +import json +from functools import partial +import re +import pytest + +# Save the Current Working Directory to find configuration files. +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../")) + +# pylint: disable=C0413 +# Import topogen and topotest helpers +from lib import topotest + +# Required to instantiate the topology builder class. +from lib.topogen import Topogen, TopoRouter, get_topogen +from lib.topolog import logger + +from lib.pim import McastTesterHelper + +pytestmark = [pytest.mark.bgpd, pytest.mark.pimd] + +app_helper = McastTesterHelper() + + +def build_topo(tgen): + """ + +----+ +----+ +----+ +----+ + | h1 | <-> | r1 | <-> | r2 | <-> | h2 | + +----+ +----+ +----+ +----+ + ^ + | + v + +----+ + | r3 | + +----+ + """ + + # Create 3 routers + for routern in range(1, 4): + tgen.add_router(f"r{routern}") + + # R1 interface eth0 and R2 interface eth0 + switch = tgen.add_switch("s1") + switch.add_link(tgen.gears["r1"]) + switch.add_link(tgen.gears["r2"]) + + # R1 interface eth1 + switch = tgen.add_switch("s2") + switch.add_link(tgen.gears["r1"]) + switch.add_link(tgen.gears["r3"]) + + # R1 interface eth2 + switch = tgen.add_switch("s3") + tgen.add_host("h1", "192.168.100.100/24", "via 192.168.100.1") + switch.add_link(tgen.gears["r1"]) + switch.add_link(tgen.gears["h1"]) + + # R2 interface eth1 + switch = tgen.add_switch("s4") + tgen.add_host("h2", "192.168.101.100/24", "via 192.168.101.1") + switch.add_link(tgen.gears["r2"]) + switch.add_link(tgen.gears["h2"]) + + +def setup_module(mod): + "Sets up the pytest environment" + tgen = Topogen(build_topo, mod.__name__) + tgen.start_topology() + + router_list = tgen.routers() + for _, router in router_list.items(): + router.load_frr_config(os.path.join(CWD, f"{router.name}/frr.conf")) + + # Initialize all routers. + tgen.start_router() + + app_helper.init(tgen) + + +def teardown_module(): + "Teardown the pytest environment" + tgen = get_topogen() + app_helper.cleanup() + tgen.stop_topology() + + +def test_bgp_convergence(): + "Wait for BGP protocol convergence" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + logger.info("waiting for protocols to converge") + + def expect_loopback_route(router, iptype, route, proto): + "Wait until route is present on RIB for protocol." + logger.info(f"waiting route {route} in {router}") + test_func = partial( + topotest.router_json_cmp, + tgen.gears[router], + f"show {iptype} route json", + {route: [{"protocol": proto}]}, + ) + _, result = topotest.run_and_expect(test_func, None, count=130, wait=1) + assertmsg = '"{}" convergence failure'.format(router) + assert result is None, assertmsg + + # Wait for R1 + expect_loopback_route("r1", "ip", "10.254.254.2/32", "bgp") + expect_loopback_route("r1", "ip", "10.254.254.3/32", "bgp") + expect_loopback_route("r1", "ipv6", "2001:db8:ffff::2/128", "bgp") + expect_loopback_route("r1", "ipv6", "2001:db8:ffff::3/128", "bgp") + + # Wait for R2 + expect_loopback_route("r2", "ip", "10.254.254.1/32", "bgp") + expect_loopback_route("r2", "ip", "10.254.254.3/32", "bgp") + expect_loopback_route("r2", "ipv6", "2001:db8:ffff::1/128", "bgp") + expect_loopback_route("r2", "ipv6", "2001:db8:ffff::3/128", "bgp") + + # Wait for R3 + expect_loopback_route("r3", "ip", "10.254.254.1/32", "bgp") + expect_loopback_route("r3", "ip", "10.254.254.2/32", "bgp") + expect_loopback_route("r3", "ipv6", "2001:db8:ffff::1/128", "bgp") + expect_loopback_route("r3", "ipv6", "2001:db8:ffff::2/128", "bgp") + + +def test_pim_convergence(): + "Wait for PIM peers find each other." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + def expect_pim_peer(router, iptype, interface, peer): + "Wait until peer is present." + logger.info(f"waiting peer {peer} in {router}") + expected = {interface: {peer: {"upTime": "*"}}} + + test_func = partial( + topotest.router_json_cmp, + tgen.gears[router], + f"show {iptype} pim neighbor json", + expected, + ) + _, result = topotest.run_and_expect(test_func, None, count=40, wait=2) + assertmsg = f'"{router}" convergence failure' + assert result is None, assertmsg + + expect_pim_peer("r1", "ip", "r1-eth0", "192.168.1.2") + expect_pim_peer("r2", "ip", "r2-eth0", "192.168.1.1") + expect_pim_peer("r1", "ip", "r1-eth1", "192.168.2.2") + + # + # IPv6 part + # + out = tgen.gears["r1"].vtysh_cmd("show interface r1-eth0 json", True) + r1_r2_link_address = out["r1-eth0"]["ipAddresses"][1]["address"].split('/')[0] + out = tgen.gears["r1"].vtysh_cmd("show interface r1-eth1 json", True) + r1_r3_link_address = out["r1-eth1"]["ipAddresses"][1]["address"].split('/')[0] + out = tgen.gears["r2"].vtysh_cmd("show interface r2-eth0 json", True) + r2_link_address = out["r2-eth0"]["ipAddresses"][1]["address"].split('/')[0] + out = tgen.gears["r3"].vtysh_cmd("show interface r3-eth0 json", True) + r3_link_address = out["r3-eth0"]["ipAddresses"][1]["address"].split('/')[0] + + expect_pim_peer("r1", "ipv6", "r1-eth0", r2_link_address) + expect_pim_peer("r2", "ipv6", "r2-eth0", r1_r2_link_address) + expect_pim_peer("r1", "ipv6", "r1-eth1", r3_link_address) + + +def test_igmp_group_limit(): + "Test IGMP group limits." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + tgen.gears["r1"].vtysh_cmd(""" + configure terminal + interface r1-eth2 + ip igmp max-groups 4 + """) + app_helper.run("h1", ["224.0.100.1", "h1-eth0"]) + app_helper.run("h1", ["224.0.100.2", "h1-eth0"]) + app_helper.run("h1", ["224.0.100.3", "h1-eth0"]) + app_helper.run("h1", ["224.0.100.4", "h1-eth0"]) + app_helper.run("h1", ["224.0.100.5", "h1-eth0"]) + app_helper.run("h1", ["224.0.100.6", "h1-eth0"]) + + def expect_igmp_group_count(): + igmp_groups = tgen.gears["r1"].vtysh_cmd("show ip igmp groups json", isjson=True) + try: + return len(igmp_groups["r1-eth2"]["groups"]) + except KeyError: + return 0 + + topotest.run_and_expect(expect_igmp_group_count, 4, count=10, wait=2) + + # Cleanup + app_helper.stop_host("h1") + tgen.gears["r1"].vtysh_cmd(""" + configure terminal + interface r1-eth2 + no ip igmp max-groups 4 + exit + clear ip igmp interfaces + """) + + +def test_igmp_group_source_limit(): + "Test IGMP source limits." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + tgen.gears["r1"].vtysh_cmd(""" + configure terminal + interface r1-eth2 + ip igmp max-sources 4 + exit + """) + + app_helper.run("h1", ["--source=192.168.100.10", "232.0.101.10", "h1-eth0"]) + app_helper.run("h1", ["--source=192.168.100.11", "232.0.101.10", "h1-eth0"]) + app_helper.run("h1", ["--source=192.168.100.12", "232.0.101.10", "h1-eth0"]) + app_helper.run("h1", ["--source=192.168.100.13", "232.0.101.10", "h1-eth0"]) + app_helper.run("h1", ["--source=192.168.100.14", "232.0.101.10", "h1-eth0"]) + app_helper.run("h1", ["--source=192.168.100.15", "232.0.101.10", "h1-eth0"]) + app_helper.run("h1", ["--source=192.168.100.16", "232.0.101.10", "h1-eth0"]) + + def expect_igmp_group_source_count(): + igmp_sources = tgen.gears["r1"].vtysh_cmd("show ip igmp sources json", isjson=True) + try: + return len(igmp_sources["r1-eth2"]["232.0.101.10"]["sources"]) + except KeyError: + return 0 + + topotest.run_and_expect(expect_igmp_group_source_count, 4, count=10, wait=2) + + # Cleanup + tgen.gears["r1"].vtysh_cmd(""" + configure terminal + interface r1-eth2 + no ip igmp max-sources 4 + exit + clear ip igmp interfaces + """) + app_helper.stop_host("h1") + + +def test_mld_group_limit(): + "Test MLD group limits." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + tgen.gears["r1"].vtysh_cmd(""" + configure terminal + interface r1-eth2 + ipv6 mld max-groups 14 + """) + app_helper.run("h1", ["FF05::100", "h1-eth0"]) + app_helper.run("h1", ["FF05::101", "h1-eth0"]) + app_helper.run("h1", ["FF05::102", "h1-eth0"]) + app_helper.run("h1", ["FF05::103", "h1-eth0"]) + app_helper.run("h1", ["FF05::104", "h1-eth0"]) + app_helper.run("h1", ["FF05::105", "h1-eth0"]) + app_helper.run("h1", ["FF05::106", "h1-eth0"]) + app_helper.run("h1", ["FF05::107", "h1-eth0"]) + app_helper.run("h1", ["FF05::108", "h1-eth0"]) + app_helper.run("h1", ["FF05::109", "h1-eth0"]) + app_helper.run("h1", ["FF05::110", "h1-eth0"]) + app_helper.run("h1", ["FF05::111", "h1-eth0"]) + app_helper.run("h1", ["FF05::112", "h1-eth0"]) + app_helper.run("h1", ["FF05::113", "h1-eth0"]) + app_helper.run("h1", ["FF05::114", "h1-eth0"]) + app_helper.run("h1", ["FF05::115", "h1-eth0"]) + + def expect_mld_group_count(): + mld_groups = tgen.gears["r1"].vtysh_cmd("show ipv6 mld groups json", isjson=True) + try: + return len(mld_groups["r1-eth2"]["groups"]) + except KeyError: + return 0 + + + topotest.run_and_expect(expect_mld_group_count, 14, count=10, wait=2) + + + # Cleanup + app_helper.stop_host("h1") + tgen.gears["r1"].vtysh_cmd(""" + configure terminal + interface r1-eth2 + no ipv6 mld max-groups 4 + exit + clear ipv6 mld interfaces + """) + + +def test_mld_group_source_limit(): + "Test MLD source limits." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + tgen.gears["r1"].vtysh_cmd(""" + configure terminal + interface r1-eth2 + ipv6 mld max-sources 4 + exit + """) + + app_helper.run("h1", ["--source=2001:db8:1::100", "FF35::100", "h1-eth0"]) + app_helper.run("h1", ["--source=2001:db8:1::101", "FF35::100", "h1-eth0"]) + app_helper.run("h1", ["--source=2001:db8:1::102", "FF35::100", "h1-eth0"]) + app_helper.run("h1", ["--source=2001:db8:1::103", "FF35::100", "h1-eth0"]) + app_helper.run("h1", ["--source=2001:db8:1::104", "FF35::100", "h1-eth0"]) + app_helper.run("h1", ["--source=2001:db8:1::105", "FF35::100", "h1-eth0"]) + app_helper.run("h1", ["--source=2001:db8:1::106", "FF35::100", "h1-eth0"]) + + def expect_mld_source_group_count(): + mld_sources = tgen.gears["r1"].vtysh_cmd("show ipv6 mld joins json", isjson=True) + try: + return len(mld_sources["default"]["r1-eth2"]["ff35::100"].keys()) + except KeyError: + return 0 + + topotest.run_and_expect(expect_mld_source_group_count, 4, count=10, wait=2) + + # Cleanup + tgen.gears["r1"].vtysh_cmd(""" + configure terminal + interface r1-eth2 + no ipv6 mld max-sources 4 + exit + clear ipv6 mld interfaces + """) + app_helper.stop_host("h1") + + +def test_memory_leak(): + "Run the memory leak test and report results." + tgen = get_topogen() + if not tgen.is_memleak_enabled(): + pytest.skip("Memory leak test/report is disabled") + + tgen.report_memory_leaks() + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args)) diff --git a/yang/frr-gmp.yang b/yang/frr-gmp.yang index 5636010d91..26b19501f9 100644 --- a/yang/frr-gmp.yang +++ b/yang/frr-gmp.yang @@ -154,6 +154,20 @@ module frr-gmp { "Enable IGMP proxy on the interface."; } + leaf max-groups { + type uint32; + default "4294967295"; + description + "Limit number of tracked IGMP group memberships on this interface."; + } + + leaf max-sources { + type uint32; + default "4294967295"; + description + "Limit number of tracked IGMPv3 sources on this interface."; + } + list join-group { key "group-addr source-addr"; description