This commit is contained in:
Rafael Zalamena 2025-04-29 16:20:43 +00:00 committed by GitHub
commit 3f3ff7facb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 945 additions and 9 deletions

View file

@ -410,6 +410,10 @@ is in a vrf, enter the interface command with the vrf keyword at the end.
Tell pim to receive IGMP reports and Query on this interface. The default
version is v3. This command is useful on a LHR.
.. clicmd:: ip igmp require-router-alert
Only accept IGMP reports with the router-alert IP option.
.. clicmd:: ip igmp join-group A.B.C.D [A.B.C.D]
Join multicast group or source-group on an interface. This will result in

View file

@ -245,6 +245,10 @@ is in a vrf, enter the interface command with the vrf keyword at the end.
Tell pim to receive MLD reports and Query on this interface. The default
version is v2. This command is useful on a LHR.
.. clicmd:: ipv6 mld require-router-alert
Only accept MLD reports with the router-alert IPv6 hop option.
.. clicmd:: ipv6 mld join X:X::X:X [Y:Y::Y:Y]
Join multicast group or source-group on an interface.

View file

@ -1845,6 +1845,18 @@ ALIAS(interface_ipv6_pim_neighbor_prefix_list,
"Restrict allowed PIM neighbors\n"
"Use prefix-list to filter neighbors\n")
DEFPY_YANG(interface_ipv6_mld_require_ra, interface_ipv6_mld_require_ra_cmd,
"[no] ipv6 mld require-router-alert",
NO_STR
IPV6_STR
IFACE_MLD_STR
"Require IP Router Alert option for MLD packets\n")
{
nb_cli_enqueue_change(vty, "./require-router-alert", NB_OP_MODIFY, no ? "false" : "true");
return nb_cli_apply_changes(vty, FRR_GMP_INTERFACE_XPATH, FRR_PIM_AF_XPATH_VAL);
}
DEFPY (show_ipv6_pim_rp,
show_ipv6_pim_rp_cmd,
"show ipv6 pim [vrf NAME] rp-info [X:X::X:X/M$group] [json$json]",
@ -3019,6 +3031,7 @@ void pim_cmd_init(void)
&interface_no_ipv6_mld_last_member_query_interval_cmd);
install_element(INTERFACE_NODE, &interface_ipv6_pim_neighbor_prefix_list_cmd);
install_element(INTERFACE_NODE, &interface_no_ipv6_pim_neighbor_prefix_list_cmd);
install_element(INTERFACE_NODE, &interface_ipv6_mld_require_ra_cmd);
install_element(VIEW_NODE, &show_ipv6_pim_rp_cmd);
install_element(VIEW_NODE, &show_ipv6_pim_rp_vrf_all_cmd);

View file

@ -1817,7 +1817,7 @@ static void gm_t_recv(struct event *t)
goto out_free;
}
if (!ip6_check_hopopts_ra(hopopts, hopopt_len, IP6_ALERT_MLD)) {
if (pim_ifp->gmp_require_ra && !ip6_check_hopopts_ra(hopopts, hopopt_len, IP6_ALERT_MLD)) {
zlog_err(log_pkt_src(
"packet without IPv6 Router Alert MLD option"));
gm_ifp->stats.rx_drop_ra++;

View file

@ -5706,6 +5706,18 @@ DEFPY_YANG(interface_ip_igmp_immediate_leave,
return nb_cli_apply_changes(vty, FRR_GMP_INTERFACE_XPATH, FRR_PIM_AF_XPATH_VAL);
}
DEFPY_YANG(interface_ip_igmp_require_ra, interface_ip_igmp_require_ra_cmd,
"[no] ip igmp require-router-alert",
NO_STR
IP_STR
IFACE_IGMP_STR
"Require IP Router Alert option for IGMP packets\n")
{
nb_cli_enqueue_change(vty, "./require-router-alert", NB_OP_MODIFY, no ? "false" : "true");
return nb_cli_apply_changes(vty, FRR_GMP_INTERFACE_XPATH, FRR_PIM_AF_XPATH_VAL);
}
DEFUN (interface_ip_pim_drprio,
interface_ip_pim_drprio_cmd,
"ip pim drpriority (0-4294967295)",
@ -9182,6 +9194,7 @@ void pim_cmd_init(void)
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_igmp_immediate_leave_cmd);
install_element(INTERFACE_NODE, &interface_ip_igmp_require_ra_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);

View file

@ -97,6 +97,7 @@ struct pim_interface {
int gm_last_member_query_count; /* IGMP or MLD last member
query count
*/
bool gmp_require_ra; /* drop IGMP without Router Alert */
struct list *gm_socket_list; /* list of struct IGMP or MLD sock */
struct list *gm_join_list; /* list of struct IGMP or MLD join */
struct list *static_group_list; /* list of struct static group */

View file

@ -726,19 +726,55 @@ bool pim_igmp_verify_header(struct ip *ip_hdr, size_t len, size_t *hlen)
return true;
}
static bool ip_check_hopopts_ra(const uint8_t *options, size_t options_len)
{
if (options_len < 4)
return false;
/*
* The values 148 and 4 were translated from the bits sequence
* from RFC 2113 Section 2.1. Syntax.
*/
if (options[0] != 148)
return false;
if (options[1] != 4)
return false;
if (options[2] != 0 && options[3] != 0)
return false;
return true;
}
int pim_igmp_packet(struct gm_sock *igmp, char *buf, size_t len)
{
const struct pim_interface *pim_interface = igmp->interface->info;
struct ip *ip_hdr = (struct ip *)buf;
size_t ip_hlen; /* ip header length in bytes */
char *igmp_msg;
int igmp_msg_len;
int msg_type;
bool router_alert;
char from_str[INET_ADDRSTRLEN];
char to_str[INET_ADDRSTRLEN];
if (!pim_igmp_verify_header(ip_hdr, len, &ip_hlen))
return -1;
if (ip_hlen > sizeof(struct ip)) {
const uint8_t *ip_options = (const uint8_t *)(ip_hdr + 1);
size_t ip_options_len = ip_hlen - sizeof(struct ip);
router_alert = ip_check_hopopts_ra(ip_options, ip_options_len);
} else
router_alert = false;
if (pim_interface->gmp_require_ra && !router_alert) {
if (PIM_DEBUG_GM_PACKETS)
zlog_debug("discarding IGMP packet from %pI4 on %s due to Router Alert option missing",
&ip_hdr->ip_src, igmp->interface->name);
return -1;
}
igmp_msg = buf + ip_hlen;
igmp_msg_len = len - ip_hlen;
msg_type = *igmp_msg;

View file

@ -756,6 +756,12 @@ const struct frr_yang_module_info frr_gmp_info = {
.modify = lib_interface_gmp_immediate_leave_modify,
}
},
{
.xpath = "/frr-interface:lib/interface/frr-gmp:gmp/address-family/require-router-alert",
.cbs = {
.modify = lib_interface_gmp_require_router_alert_modify,
}
},
{
.xpath = "/frr-interface:lib/interface/frr-gmp:gmp/address-family/static-group",
.cbs = {

View file

@ -292,6 +292,7 @@ int lib_interface_gmp_address_family_static_group_destroy(
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);
int lib_interface_gmp_immediate_leave_modify(struct nb_cb_modify_args *args);
int lib_interface_gmp_require_router_alert_modify(struct nb_cb_modify_args *args);
/*
* Callback registered with routing_nb lib to validate only

View file

@ -4561,6 +4561,29 @@ int lib_interface_gmp_immediate_leave_modify(struct nb_cb_modify_args *args)
return NB_OK;
}
/*
* XPath: /frr-interface:lib/interface/frr-gmp:gmp/address-family/require-router-alert
*/
int lib_interface_gmp_require_router_alert_modify(struct nb_cb_modify_args *args)
{
struct interface *ifp;
struct pim_interface *pim_ifp;
switch (args->event) {
case NB_EV_VALIDATE:
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->gmp_require_ra = yang_dnode_get_bool(args->dnode, NULL);
break;
}
return NB_OK;
}
/*
* XPath: /frr-interface:lib/interface/frr-gmp:gmp/address-family/proxy
*/

View file

@ -291,6 +291,12 @@ static int gm_config_write(struct vty *vty, int writes,
++writes;
}
/* IF ip igmp require-router-alert */
if (pim_ifp->gmp_require_ra) {
vty_out(vty, " ip igmp require-router-alert\n");
++writes;
}
if (pim_ifp->gm_proxy) {
vty_out(vty, " ip igmp proxy\n");
++writes;
@ -380,6 +386,12 @@ static int gm_config_write(struct vty *vty, int writes,
++writes;
}
/* IF ip igmp require-router-alert */
if (pim_ifp->gmp_require_ra) {
vty_out(vty, " ipv6 mld require-router-alert\n");
++writes;
}
if (pim_ifp->mld_version != MLD_DEFAULT_VERSION)
vty_out(vty, " ipv6 mld version %d\n", pim_ifp->mld_version);

View file

@ -0,0 +1,58 @@
#!/usr/bin/env python
#
# SPDX-License-Identifier: BSD-2-Clause
#
# igmp.py
# Part of NetDEF CI System
#
# Copyright (c) 2025 by
# Network Device Education Foundation, Inc. ("NetDEF")
#
import struct
from scapy.all import Packet
from scapy.layers.inet import IP, IPOption_Router_Alert
from scapy.layers.l2 import Ether
from scapy.packet import bind_layers
from scapy.sendrecv import sendp
def calculate_checksum(packet):
if len(packet) % 2 == 1:
packet += b'\0'
s = sum(struct.unpack("!%dH" % (len(packet) // 2), packet))
s = (s >> 16) + (s & 0xffff)
s += s >> 16
return ~s & 0xffff
class IGMP(Packet):
"""
Base class for creating and manipulating IGMP packets.
Methods:
__init__(self, version=1, type=0x11, chksum=None, gaddr="0.0.0.0", src_ip="192.168.100.1", *args, **kwargs):
Initializes an IGMP packet with the given parameters.
send(self, iface, count=1, interval=0):
Sends the IGMP packet on the specified interface.
enable_router_alert(self):
Enables the Router Alert option for the IGMP packet.
"""
def enable_router_alert(self):
router_alert = IPOption_Router_Alert()
self.options.append(router_alert)
def post_build(self, p, pay):
if self.chksum is None:
chksum = calculate_checksum(p)
p = p[:2] + struct.pack("!H", chksum) + p[4:]
return p + pay
def send(self, interval=0, count=1, iface="eth0"):
bind_layers(IP, IGMP, proto=2)
if self.options:
packet = Ether() / IP(dst=self.gaddr, tos=0xc0, id=0, ttl=1, src=self.src_ip, options=self.options, proto=2, frag=0) / self
else:
packet = Ether() / IP(dst=self.gaddr, tos=0xc0, id=0, ttl=1, src=self.src_ip, proto=2, frag=0) / self
sendp(packet, inter=int(interval), iface=iface, count=int(count))

View file

@ -0,0 +1,73 @@
#!/usr/bin/env python
#
# SPDX-License-Identifier: BSD-2-Clause
#
# igmp_v1.py
# Part of NetDEF CI System
#
# Copyright (c) 2025 by
# Network Device Education Foundation, Inc. ("NetDEF")
#
import argparse
from igmp import IGMP
from scapy.all import ByteField, IPField
from scapy.fields import XShortField, BitField
class IGMPv1(IGMP) :
"""
Represents an IGMPv1 packet.
Attributes:
version (int): IGMP version (default is 1).
type (int): The type of the IGMP message (default is 0x11).
unused (int): The maximum response time (default is 0).
chksum (int): Checksum of the packet.
gaddr (str): The group address (default is "0.0.0.0").
src_ip (str): Source IP address (default is "192.168.100.1").
options (list): Additional options for the packet.
Methods:
__init__(self, version=1, type=0x11, unused=0, chksum=None, gaddr="0.0.0.0", src_ip="192.168.100.1", *args, **kwargs):
Initializes an IGMPv1 packet with the given parameters.
"""
name = "IGMPv1"
fields_desc = [
BitField("version", 1, 4),
BitField("type", 0x11, 4),
ByteField("unused", 0),
XShortField("chksum", None),
IPField("gaddr", "0.0.0.0")
]
def __init__(self, version=1, type=0x11, unused=0, chksum=None, gaddr="0.0.0.0", src_ip="192.168.100.1", *args, **kwargs):
super().__init__(*args, **kwargs)
self.version = version
self.type = type
self.unused = unused
self.chksum = chksum
self.gaddr = gaddr
self.src_ip = src_ip
self.options = []
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Send an IGMPv1 packet")
parser.add_argument("--gaddr", type=str, default="224.0.0.1", help="Group address")
parser.add_argument("--src_ip", type=str, default="192.168.1.10", help="Source IP address")
parser.add_argument("--type", type=lambda x: int(x, 0), default=0x11, help="Type of IGMP message")
parser.add_argument("--enable_router_alert", action="store_true", help="Enable Router Alert option")
parser.add_argument("--iface", type=str, default="eth0", help="Network interface to send the packet")
parser.add_argument("--count", type=int, default=1, help="Number of packets to send")
parser.add_argument("--interval", type=int, default=0, help="Interval between packets")
args = parser.parse_args()
igmp_packet = IGMPv1(gaddr=args.gaddr, src_ip=args.src_ip, type=args.type)
if args.enable_router_alert:
igmp_packet.enable_router_alert()
igmp_packet.send(iface=args.iface, count=args.count, interval=args.interval)

View file

@ -0,0 +1,72 @@
#!/usr/bin/env python
#
# SPDX-License-Identifier: BSD-2-Clause
#
# imgp_v2.py
# Part of NetDEF CI System
#
# Copyright (c) 2025 by
# Network Device Education Foundation, Inc. ("NetDEF")
#
import argparse
from scapy.all import ByteField, ShortField, IPField
from scapy.fields import BitField
from igmp import IGMP
class IGMPv2(IGMP):
"""
IGMPv2 class for creating and manipulating IGMP version 2 packets.
Attributes:
name (str): Name of the protocol.
fields_desc (list): List of fields in the IGMPv2 packet.
version (int): IGMP version.
type (int): Type of IGMP message.
max_resp_time (int): Maximum response time.
chksum (int): Checksum of the packet.
gaddr (str): Group address.
src_ip (str): Source IP address.
options (list): Additional options for the packet.
Methods:
__init__(self, version=1, type=0x11, max_resp_time=10, chksum=None, gaddr="0.0.0.0", src_ip="192.168.100.1", *args, **kwargs):
Initializes an IGMPv2 packet with the given parameters.
"""
name = "IGMPv2"
fields_desc = [
BitField("version", 1, 4),
BitField("type", 0x11, 4),
ByteField("max_resp_time", 10),
ShortField("checksum", None),
IPField("gaddr", "0.0.0.0")
]
def __init__(self, version=1, type=0x11, max_resp_time=10, chksum=None, gaddr="0.0.0.0", src_ip="192.168.100.1", *args, **kwargs):
super().__init__(*args, **kwargs)
self.version = version
self.type = type
self.max_resp_time = max_resp_time
self.chksum = chksum
self.gaddr = gaddr
self.src_ip = src_ip
self.options = []
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Send an IGMPv2 packet")
parser.add_argument("--gaddr", type=str, default="224.0.0.1", help="Group address")
parser.add_argument("--src_ip", type=str, default="192.168.1.10", help="Source IP address")
parser.add_argument("--type", type=lambda x: int(x, 0), default=0x11, help="Type of IGMP message")
parser.add_argument("--enable_router_alert", action="store_true", help="Enable Router Alert option")
parser.add_argument("--iface", type=str, default="eth0", help="Network interface to send the packet")
parser.add_argument("--count", type=int, default=1, help="Number of packets to send")
parser.add_argument("--interval", type=int, default=0, help="Interval between packets")
args = parser.parse_args()
igmp_packet = IGMPv2(gaddr=args.gaddr, src_ip=args.src_ip, type=args.type)
if args.enable_router_alert:
igmp_packet.enable_router_alert()
igmp_packet.send(iface=args.iface, count=args.count, interval=args.interval)

View file

@ -0,0 +1,78 @@
#!/usr/bin/env python
#
# SPDX-License-Identifier: BSD-2-Clause
#
# imgp_v3.py
# Part of NetDEF CI System
#
# Copyright (c) 2025 by
# Network Device Education Foundation, Inc. ("NetDEF")
#
import argparse
from scapy.all import ByteField, ShortField, IPField
from scapy.contrib.igmpv3 import IGMPv3gr
from scapy.fields import BitField, PacketListField
from scapy.layers.inet6 import ICMPv6MLDMultAddrRec
from igmp import IGMP
class IGMPv3(IGMP):
name = "IGMPv3"
fields_desc = [
BitField("type", 0x22, 8),
BitField("reserved1", None, 8),
ShortField("checksum", None),
ShortField("reserved2", None),
ShortField("records_number", None),
PacketListField("records",
[],
IGMPv3gr,
count_from=lambda p: p.records_number)
]
def __init__(self, version=3, type=0x22, max_resp_time=10,
chksum=None, records=[], maddrs=[], rtype=1, gaddr="224.0.0.22",
src_ip="192.168.100.1", *args, **kwargs):
super().__init__(*args, **kwargs)
self.version = version
self.type = type
self.max_resp_time = max_resp_time
self.chksum = chksum
self.src_ip = src_ip
self.options = []
self.gaddr = gaddr
num_maddrs = len(maddrs)
grouped_sources = [[] for _ in range(num_maddrs)]
for index, source in enumerate(records):
grouped_sources[index % num_maddrs].append(source)
for maddr, sources in zip(maddrs, grouped_sources):
self.records.append(IGMPv3gr(numsrc=len(sources), srcaddrs=sources, maddr=maddr, rtype=rtype))
self.records_number = num_maddrs
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Send an IGMPv3 packet")
parser.add_argument("--gaddr", type=str, default="224.0.0.22", help="Destination IP address")
parser.add_argument("--maddr", action='append', default=[], help="Multicast Address Records")
parser.add_argument("--src_ip", type=str, default="192.168.1.10", help="Source IP address")
parser.add_argument("--type", type=lambda x: int(x, 0), default=0x22, help="Type of IGMP message")
parser.add_argument("--rtype", type=int, default=1, help="Record type")
parser.add_argument("--enable_router_alert", action="store_true", help="Enable Router Alert option")
parser.add_argument("--iface", type=str, default="eth0", help="Network interface to send the packet")
parser.add_argument("--count", type=int, default=1, help="Number of packets to send")
parser.add_argument("--interval", type=int, default=0, help="Interval between packets")
args = parser.parse_args()
igmp_packet = IGMPv3(maddrs=args.maddr,
src_ip=args.src_ip,
type=args.type,
gaddr=args.gaddr,
rtype=args.rtype)
if args.enable_router_alert:
igmp_packet.enable_router_alert()
igmp_packet.send(iface=args.iface, count=args.count, interval=args.interval)

View file

@ -0,0 +1,49 @@
#!/usr/bin/env python
#
# SPDX-License-Identifier: BSD-2-Clause
#
# mld.py
# Part of NetDEF CI System
#
# Copyright (c) 2025 by
# Network Device Education Foundation, Inc. ("NetDEF")
#
from scapy.layers.inet6 import IPv6, IPv6ExtHdrHopByHop, _ICMPv6ML, RouterAlert
from scapy.layers.l2 import Ether
from scapy.packet import bind_layers
from scapy.sendrecv import sendp
class MLD(_ICMPv6ML):
"""
MLD is a class representing a Multicast Listener Discovery (MLD) packet.
Attributes:
name (str): Name of the packet, default is "MLD".
options (list): List of options, default is an empty list.
mladdr (str): Multicast address, default is "::".
src_ip (str): Source IP address, default is "fe80::1".
Methods:
enable_router_alert(self):
Enables the Router Alert option for the MLD packet.
send(self, interval=0, count=1, iface="eth0"):
Sends the MLD packet on the specified network interface.
"""
name = "MLD"
def enable_router_alert(self):
router_alert = RouterAlert(value=0)
self.options.append(router_alert)
def send(self, interval=0, count=1, iface="eth0"):
bind_layers(IPv6, MLD, nh=58) # nh=58 for ICMPv6
if self.options:
packet = Ether() / IPv6(dst=self.dst_ip, src=self.src_ip, hlim=1) / IPv6ExtHdrHopByHop(options=self.options) / self
else:
packet = Ether() / IPv6(dst=self.dst_ip, src=self.src_ip, hlim=1) / self
sendp(packet, inter=int(interval), iface=iface, count=int(count))

View file

@ -0,0 +1,91 @@
#!/usr/bin/env python
#
# SPDX-License-Identifier: BSD-2-Clause
#
# mld_v1.py
# Part of NetDEF CI System
#
# Copyright (c) 2025 by
# Network Device Education Foundation, Inc. ("NetDEF")
#
import argparse
from scapy.fields import BitField, XShortField, IP6Field, ByteField, ByteEnumField, ShortField
from scapy.layers.inet6 import icmp6types
from mld import MLD
class MLDv1(MLD):
"""
MLDv1 is a class representing an MLD (Multicast Listener Discovery) version 1 packet.
Attributes:
type (int): Type of MLD message, default is 0x11.
code (int): Code of MLD message, default is 0.
cksum (int): Checksum of the packet, default is None.
mrd (int): Maximum response delay, default is 0.
reserved (int): Reserved field, default is 0.
mladdr (str): Multicast address, default is "::".
src_ip (str): Source IP address, default is "fe80::1".
dst_ip (str): Destination IP address, default is "fe80::2".
options (list): List of options, default is an empty list.
Methods:
__init__(self, type=0x11, code=0, max_response_delay=0, chksum=None, gaddr="ff02::1", src_ip="fe80::1", dst_ip="fe80::2", *args, **kwargs):
Initializes an MLDv1 packet with the given parameters.
send(self, iface="eth0", count=1, interval=0):
Sends the MLDv1 packet on the specified network interface.
"""
name = "MLDv1"
fields_desc = [
ByteEnumField("type", 130, icmp6types),
ByteField("code", 0),
XShortField("cksum", None),
ShortField("mrd", 0),
ShortField("reserved", 0),
IP6Field("mladdr", "::")
]
def __init__(self, type=0x11, code=0, max_response_delay=0, chksum=None, gaddr="ff02::1", src_ip="fe80::1", dst_ip="ff02::16", *args, **kwargs):
super().__init__(*args, **kwargs)
self.type = type
self.code = code
self.mrd = max_response_delay
self.cksum = chksum
self.reserved = 0
self.mladdr = gaddr
self.src_ip = src_ip
self.dst_ip = dst_ip
self.options = []
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Send an MLDv1 packet")
parser.add_argument("--type", type=lambda x: int(x, 0), default=0x83, help="Type of MLD message")
parser.add_argument("--code", type=int, default=0, help="Code of MLD message")
parser.add_argument("--max_response_delay", type=int, default=0, help="Maximum response delay")
parser.add_argument("--chksum", type=int, default=None, help="Checksum of the packet")
parser.add_argument("--gaddr", type=str, default="ff02::1", help="Group address")
parser.add_argument("--src_ip", type=str, default="fe80::1", help="Source IP address")
parser.add_argument("--dst_ip", type=str, default="ff02::16", help="Destination IP address")
parser.add_argument("--enable_router_alert", action="store_true", help="Enable Router Alert option")
parser.add_argument("--iface", type=str, default="eth0", help="Network interface to send the packet")
parser.add_argument("--count", type=int, default=1, help="Number of packets to send")
parser.add_argument("--interval", type=int, default=0, help="Interval between packets")
args = parser.parse_args()
mld_packet = MLDv1(gaddr=args.gaddr,
src_ip=args.src_ip,
dst_ip=args.dst_ip,
type=args.type,
code=args.code,
max_response_delay=args.max_response_delay)
if args.enable_router_alert:
mld_packet.enable_router_alert()
mld_packet.send(iface=args.iface, count=args.count, interval=args.interval)

View file

@ -0,0 +1,118 @@
#!/usr/bin/env python
#
# SPDX-License-Identifier: BSD-2-Clause
#
# mld_v2.py
# Part of NetDEF CI System
#
# Copyright (c) 2025 by
# Network Device Education Foundation, Inc. ("NetDEF")
#
import argparse
from scapy.fields import BitField, XShortField, IP6Field, ByteField, ByteEnumField, ShortField, PacketListField
from scapy.layers.inet6 import icmp6types, ICMPv6MLDMultAddrRec
from mld import MLD
class MLDv2(MLD):
"""
MLDv2 (Multicast Listener Discovery version 2) class for creating and handling MLDv2 packets.
Inherits from:
MLD: Base class for MLD packets.
Attributes:
name (str): Name of the packet type.
fields_desc (list): List of fields in the packet.
type (int): Type of MLD message.
code (int): Code of MLD message.
cksum (int): Checksum of the packet.
reserved (int): Reserved field.
src_ip (str): Source IP address.
dst_ip (str): Destination IP address.
options (list): List of options for the packet.
records (list): List of multicast address records.
records_number (int): Number of multicast address records.
Record Type values:
1: MODE_IS_INCLUDE
2: MODE_IS_EXCLUDE
3: CHANGE_TO_INCLUDE_MODE
4: CHANGE_TO_EXCLUDE_MODE
5: ALLOW_NEW_SOURCES
6: BLOCK_OLD_SOURCES
Methods:
__init__(self, proto_type=143, code=0, rtype=1, chksum=None, src_ip="fe80::1", dst_ip="ff02::fb", records=[], *args, **kwargs):
Initializes an MLDv2 packet with the given parameters.
enable_router_alert(self):
Enables the Router Alert option for the packet.
send(self, iface="eth0", count=1, interval=0):
Sends the MLDv2 packet on the specified network interface.
"""
name = "MLDv2"
fields_desc = [
ByteEnumField("type", 143, icmp6types),
ByteField("code", 0),
XShortField("cksum", None),
BitField("reserved", 0, 16),
BitField("records_number", 0, 16),
PacketListField("records",
[],
ICMPv6MLDMultAddrRec,
count_from=lambda p: p.records_number)
]
def __init__(self, proto_type=143, code=0, rtype=1,
chksum=None, src_ip="fe80::1", dst_ip="ff02::16", maddrs=[], *args, **kwargs):
super().__init__(*args, **kwargs)
self.type = proto_type
self.code = code
self.cksum = chksum
self.reserved = 0
self.src_ip = src_ip
self.dst_ip = dst_ip
self.options = []
num_maddrs = len(maddrs)
grouped_sources = [[] for _ in range(num_maddrs)]
for index, source in enumerate(maddrs):
grouped_sources[index % num_maddrs].append(source)
for maddr, sources in zip(maddrs, grouped_sources):
self.records.append(ICMPv6MLDMultAddrRec(dst=maddr, rtype=rtype))
self.records_number = num_maddrs
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Send an MLDv2 packet")
parser.add_argument("--type", type=int, default=143, help="Type of MLD message")
parser.add_argument("--code", type=int, default=0, help="Code of MLD message")
parser.add_argument("--chksum", type=int, default=None, help="Checksum of the packet")
parser.add_argument("--src_ip", type=str, default="fe80::1", help="Source IP address")
parser.add_argument("--dst_ip", type=str, default="ff02::16", help="Destination IP address")
parser.add_argument("--maddr", action='append', default=[], help="Multicast Address Records")
parser.add_argument("--rtype", type=int, default=2, help="Record type")
parser.add_argument("--enable_router_alert", action="store_true", help="Enable Router Alert option")
parser.add_argument("--iface", type=str, default="eth0", help="Network interface to send the packet")
parser.add_argument("--count", type=int, default=1, help="Number of packets to send")
parser.add_argument("--interval", type=int, default=0, help="Interval between packets")
args = parser.parse_args()
packet = MLDv2(maddrs=args.maddr,
src_ip=args.src_ip,
dst_ip=args.dst_ip,
rtype=args.rtype,
proto_type=args.type,
code=args.code)
if args.enable_router_alert:
packet.enable_router_alert()
packet.send(iface=args.iface, count=args.count, interval=args.interval)

View file

@ -174,32 +174,36 @@ def test_pim_convergence():
# This neighbor is denied by default
expect_pim_peer("r1", "ip", "r1-eth1", "192.168.2.2", missing=True)
# Lets configure the prefix list so the above neighbor gets accepted:
tgen.gears["r1"].vtysh_cmd("""
tgen.gears["r1"].vtysh_cmd(
"""
configure terminal
ip prefix-list pim-eth0-neighbors permit 192.168.2.0/24
""")
"""
)
expect_pim_peer("r1", "ip", "r1-eth1", "192.168.2.2", missing=False)
#
# 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]
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]
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]
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]
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, missing=True)
tgen.gears["r1"].vtysh_cmd(f"""
tgen.gears["r1"].vtysh_cmd(
f"""
configure terminal
ipv6 prefix-list pimv6-eth0-neighbors permit {r3_link_address}/64
""")
"""
)
expect_pim_peer("r1", "ipv6", "r1-eth1", r3_link_address, missing=False)
@ -502,6 +506,279 @@ def test_mldv1_immediate_leave():
app_helper.stop_host("h3")
def host_send_igmp_packet(host, script, type, source, group, router_alert=True):
"Sends packet using specified script from host."
command = f"python3 {CWD}/../lib/packet/{script}"
command += f" --src_ip={source} --gaddr={group}"
command += f" --iface={host}-eth0 --type={type}"
if router_alert:
command += f" --enable_router_alert"
tgen = get_topogen()
tgen.gears[host].run(command)
def host_send_igmpv3_packet(host, source, group, router_alert=True):
"Sends packet using specified script from host."
command = f"python3 {CWD}/../lib/packet/igmp/igmp_v3.py"
command += f" --src_ip={source} --iface={host}-eth0"
command += f" --maddr={group} --rtype=2"
if router_alert:
command += f" --enable_router_alert"
tgen = get_topogen()
tgen.gears[host].run(command)
def expect_igmp_group(router, interface, group, missing=False):
tgen = get_topogen()
igmp_groups = tgen.gears[router].vtysh_cmd("show ip igmp groups json", isjson=True)
try:
for entry in igmp_groups[interface]["groups"]:
if entry["group"] == group:
return True
return False
except KeyError:
return False
def test_igmp_router_alert():
"Test IGMP router alert check feature."
tgen = get_topogen()
if tgen.routers_have_failure():
pytest.skip(tgen.errors)
#
# Test that without require-router-alert we learn IGMP groups
#
source = "192.168.100.100"
group = "224.100.10.10"
host_send_igmp_packet(
"h1", "igmp/igmp_v1.py", 0x12, source, group, router_alert=False
)
test_func = partial(expect_igmp_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv, "failed to learn group using IGMPv1 without router alert"
group = "224.100.10.11"
host_send_igmp_packet(
"h1", "igmp/igmp_v2.py", 0x16, source, group, router_alert=False
)
test_func = partial(expect_igmp_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv, "failed to learn group using IGMPv2 without router alert"
group = "224.100.10.12"
host_send_igmpv3_packet("h1", source, group, False)
test_func = partial(expect_igmp_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv, "failed to learn group using IGMPv3 without router alert"
#
# Test that with require-router-alert we don't learn IGMP groups
#
tgen.gears["r1"].vtysh_cmd(
"""
configure terminal
interface r1-eth2
ip igmp require-router-alert
"""
)
source = "192.168.100.100"
group = "224.100.10.20"
host_send_igmp_packet(
"h1", "igmp/igmp_v1.py", 0x12, source, group, router_alert=False
)
test_func = partial(expect_igmp_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to not learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv == False, "failed to learn group using IGMPv1 without router alert"
group = "224.100.10.21"
host_send_igmp_packet(
"h1", "igmp/igmp_v2.py", 0x16, source, group, router_alert=False
)
test_func = partial(expect_igmp_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to not learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv == False, "failed to learn group using IGMPv2 without router alert"
group = "224.100.10.22"
host_send_igmpv3_packet("h1", source, group, False)
test_func = partial(expect_igmp_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to not learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv == False, "failed to learn group using IGMPv3 without router alert"
#
# Test that with require-router-alert we learn IGMP groups
#
source = "192.168.100.100"
group = "224.100.10.30"
host_send_igmp_packet(
"h1", "igmp/igmp_v1.py", 0x12, source, group, router_alert=True
)
test_func = partial(expect_igmp_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv, "failed to learn group using IGMPv1 without router alert"
group = "224.100.10.31"
host_send_igmp_packet(
"h1", "igmp/igmp_v2.py", 0x16, source, group, router_alert=True
)
test_func = partial(expect_igmp_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv, "failed to learn group using IGMPv2 without router alert"
group = "224.100.10.32"
host_send_igmpv3_packet("h1", source, group, True)
test_func = partial(expect_igmp_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv, "failed to learn group using IGMPv3 without router alert"
tgen.gears["r1"].vtysh_cmd(
"""
configure terminal
interface r1-eth2
no ip igmp require-router-alert
"""
)
def host_send_mldv1_packet(host, source, group, router_alert=True):
"Sends packet using specified script from host."
command = f"python3 {CWD}/../lib/packet/mld/mld_v1.py"
command += f" --src_ip={source} --gaddr={group}"
command += f" --iface={host}-eth0"
if router_alert:
command += f" --enable_router_alert"
tgen = get_topogen()
tgen.gears[host].run(command)
def host_send_mldv2_packet(host, source, group, router_alert=True):
"Sends packet using specified script from host."
command = f"python3 {CWD}/../lib/packet/mld/mld_v2.py"
command += f" --src_ip={source} --iface={host}-eth0"
command += f" --maddr={group} --rtype=2"
if router_alert:
command += f" --enable_router_alert"
tgen = get_topogen()
tgen.gears[host].run(command)
def expect_mld_group(router, interface, group, missing=False):
tgen = get_topogen()
igmp_groups = tgen.gears[router].vtysh_cmd("show ipv6 mld groups json", isjson=True)
try:
for entry in igmp_groups[interface]["groups"]:
if entry["group"] == group:
return True
return False
except KeyError:
return False
def test_mld_router_alert():
"Test IGMP router alert check feature."
tgen = get_topogen()
if tgen.routers_have_failure():
pytest.skip(tgen.errors)
addr_out = json.loads(tgen.gears["h1"].run("ip -j addr show dev h1-eth0"))
source = None
for address in addr_out[0]["addr_info"]:
if address["family"] != "inet6":
continue
if address["scope"] != "link":
continue
source = address["local"]
break
assert source is not None, "failed to find link-local address"
#
# Test that without require-router-alert we learn MLD groups
#
group = "ff05::100"
host_send_mldv1_packet("h1", source, group, router_alert=False)
test_func = partial(expect_mld_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv, "failed to learn group using MLDv1 without router alert"
group = "ff05::101"
host_send_mldv2_packet("h1", source, group, router_alert=False)
test_func = partial(expect_mld_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv, "failed to learn group using MLDv2 without router alert"
#
# Test that with require-router-alert we don't learn MLD groups
#
tgen.gears["r1"].vtysh_cmd(
"""
configure terminal
interface r1-eth2
ipv6 mld require-router-alert
"""
)
group = "ff05::110"
host_send_mldv1_packet("h1", source, group, router_alert=False)
test_func = partial(expect_mld_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv == False, "failed to learn group using MLDv1 without router alert"
group = "ff05::111"
host_send_mldv2_packet("h1", source, group, router_alert=False)
test_func = partial(expect_mld_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv == False, "failed to learn group using MLDv2 without router alert"
#
# Test that with require-router-alert we learn MLD groups
#
group = "ff05::120"
host_send_mldv1_packet("h1", source, group, router_alert=True)
test_func = partial(expect_mld_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv, "failed to learn group using MLDv1 without router alert"
group = "ff05::121"
host_send_mldv2_packet("h1", source, group, router_alert=True)
test_func = partial(expect_mld_group, "r1", "r1-eth2", group)
logger.info(f"Waiting for r1 to learn {group} in interface r1-eth2")
rv, _ = topotest.run_and_expect(test_func, True, count=10, wait=2)
assert rv, "failed to learn group using MLDv2 without router alert"
tgen.gears["r1"].vtysh_cmd(
"""
configure terminal
interface r1-eth2
no ipv6 mld require-router-alert
"""
)
def test_memory_leak():
"Run the memory leak test and report results."
tgen = get_topogen()

View file

@ -194,6 +194,13 @@ module frr-gmp {
Has no effect when IGMPv3/MLDv2 is in use.";
}
leaf require-router-alert {
type boolean;
default "false";
description
"Only process IGMP packets with IP Router Alert option set.";
}
list static-group {
key "group-addr source-addr";
description