+ h10 = VppHostState(
+ self,
+ IGMP_FILTER.INCLUDE,
+ self.pg0.sw_if_index,
+ IgmpSG("238.1.1.3", src_list),
+ )
+ h10.add_vpp_config()
+
+ capture = self.pg0.get_capture(2, timeout=10)
+ # wait for a little bit
+ self.sleep(1)
+
+ #
+ # remove state, expect the report for the removal
+ # the dump should be empty
+ #
+ self.vapi.sw_interface_set_mtu(self.pg0.sw_if_index, [600, 0, 0, 0])
+ self.remove_group(h8)
+ self.remove_group(h9)
+ self.remove_group(h2)
+ self.remove_group(h3)
+ self.remove_group(h4)
+ self.remove_group(h5)
+ self.remove_group(h6)
+ self.remove_group(h7)
+ self.remove_group(h10)
+
+ self.logger.info(self.vapi.cli("sh igmp config"))
+ self.assertFalse(self.vapi.igmp_dump())
+
+ #
+ # TODO
+ # ADD STATE ON MORE INTERFACES
+ #
+
+ self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 0, IGMP_MODE.HOST)
+
+ def test_igmp_router(self):
+ """IGMP Router Functions"""
+
+ #
+ # Drop reports when not enabled
+ #
+ p_j = (
+ Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+ / IP(
+ src=self.pg0.remote_ip4,
+ dst="224.0.0.22",
+ tos=0xC0,
+ ttl=1,
+ options=[
+ IPOption(
+ copy_flag=1, optclass="control", option="router_alert", length=4
+ )
+ ],
+ )
+ / IGMPv3(type="Version 3 Membership Report")
+ / IGMPv3mr(numgrp=1)
+ / IGMPv3gr(
+ rtype="Allow New Sources",
+ maddr="239.1.1.1",
+ srcaddrs=["10.1.1.1", "10.1.1.2"],
+ )
+ )
+ p_l = (
+ Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+ / IP(
+ src=self.pg0.remote_ip4,
+ dst="224.0.0.22",
+ tos=0xC0,
+ options=[
+ IPOption(
+ copy_flag=1, optclass="control", option="router_alert", length=4
+ )
+ ],
+ )
+ / IGMPv3(type="Version 3 Membership Report")
+ / IGMPv3mr(numgrp=1)
+ / IGMPv3gr(
+ rtype="Block Old Sources",
+ maddr="239.1.1.1",
+ srcaddrs=["10.1.1.1", "10.1.1.2"],
+ )
+ )
+
+ self.send(self.pg0, p_j)
+ self.assertFalse(self.vapi.igmp_dump())
+
+ #
+ # drop the default timer values so these tests execute in a
+ # reasonable time frame
+ #
+ self.vapi.cli("test igmp timers query 1 src 3 leave 1")
+
+ #
+ # enable router functions on the interface
+ #
+ self.pg_enable_capture(self.pg_interfaces)
+ self.pg_start()
+ self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 1, IGMP_MODE.ROUTER)
+ self.vapi.want_igmp_events(1)
+
+ #
+ # wait for router to send general query
+ #
+ for ii in range(3):
+ capture = self.pg0.get_capture(1, timeout=2)
+ self.verify_general_query(capture[0])
+ self.pg_enable_capture(self.pg_interfaces)
+ self.pg_start()
+
+ #
+ # re-send the report. VPP should now hold state for the new group
+ # VPP sends a notification that a new group has been joined
+ #
+ self.send(self.pg0, p_j)
+
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.1", 1)
+ )
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 1)
+ )
+ dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
+ self.assertEqual(len(dump), 2)
+ self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", "10.1.1.1"))
+ self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", "10.1.1.2"))
+
+ #
+ # wait for the per-source timer to expire
+ # the state should be reaped
+ # VPP sends a notification that the group has been left
+ #
+ self.assertTrue(
+ wait_for_igmp_event(self, 4, self.pg0, "239.1.1.1", "10.1.1.1", 0)
+ )
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 0)
+ )
+ self.assertFalse(self.vapi.igmp_dump())
+
+ #
+ # resend the join. wait for two queries and then send a current-state
+ # record to include all sources. this should reset the expiry time
+ # on the sources and thus they will still be present in 2 seconds time.
+ # If the source timer was not refreshed, then the state would have
+ # expired in 3 seconds.
+ #
+ self.send(self.pg0, p_j)
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.1", 1)
+ )
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 1)
+ )
+ dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
+ self.assertEqual(len(dump), 2)
+
+ capture = self.pg0.get_capture(2, timeout=3)
+ self.verify_general_query(capture[0])
+ self.verify_general_query(capture[1])
+
+ p_cs = (
+ Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+ / IP(
+ src=self.pg0.remote_ip4,
+ dst="224.0.0.22",
+ tos=0xC0,
+ options=[
+ IPOption(
+ copy_flag=1, optclass="control", option="router_alert", length=4
+ )
+ ],
+ )
+ / IGMPv3(type="Version 3 Membership Report")
+ / IGMPv3mr(numgrp=1)
+ / IGMPv3gr(
+ rtype="Mode Is Include",
+ maddr="239.1.1.1",
+ srcaddrs=["10.1.1.1", "10.1.1.2"],
+ )
+ )
+
+ self.send(self.pg0, p_cs)
+
+ self.sleep(2)
+ dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
+ self.assertEqual(len(dump), 2)
+ self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", "10.1.1.1"))
+ self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", "10.1.1.2"))
+
+ #
+ # wait for the per-source timer to expire
+ # the state should be reaped
+ #
+ self.assertTrue(
+ wait_for_igmp_event(self, 4, self.pg0, "239.1.1.1", "10.1.1.1", 0)
+ )
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 0)
+ )
+ self.assertFalse(self.vapi.igmp_dump())
+
+ #
+ # resend the join, then a leave. Router sends a group+source
+ # specific query containing both sources
+ #
+ self.send(self.pg0, p_j)
+
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.1", 1)
+ )
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 1)
+ )
+ dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
+ self.assertEqual(len(dump), 2)
+
+ self.send(self.pg0, p_l)
+ capture = self.pg0.get_capture(1, timeout=3)
+ self.verify_group_query(capture[0], "239.1.1.1", ["10.1.1.1", "10.1.1.2"])
+
+ #
+ # the group specific query drops the timeout to leave (=1) seconds
+ #
+ self.assertTrue(
+ wait_for_igmp_event(self, 2, self.pg0, "239.1.1.1", "10.1.1.1", 0)
+ )
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 0)
+ )
+ self.assertFalse(self.vapi.igmp_dump())
+ self.assertFalse(self.vapi.igmp_dump())
+
+ #
+ # a TO_EX({}) / IN_EX({}) is treated like a (*,G) join
+ #
+ p_j = (
+ Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+ / IP(
+ src=self.pg0.remote_ip4,
+ dst="224.0.0.22",
+ tos=0xC0,
+ ttl=1,
+ options=[
+ IPOption(
+ copy_flag=1, optclass="control", option="router_alert", length=4
+ )
+ ],
+ )
+ / IGMPv3(type="Version 3 Membership Report")
+ / IGMPv3mr(numgrp=1)
+ / IGMPv3gr(rtype="Change To Exclude Mode", maddr="239.1.1.2")
+ )
+
+ self.send(self.pg0, p_j)
+
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.2", "0.0.0.0", 1)
+ )
+
+ p_j = (
+ Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+ / IP(
+ src=self.pg0.remote_ip4,
+ dst="224.0.0.22",
+ tos=0xC0,
+ ttl=1,
+ options=[
+ IPOption(
+ copy_flag=1, optclass="control", option="router_alert", length=4
+ )
+ ],
+ )
+ / IGMPv3(type="Version 3 Membership Report")
+ / IGMPv3mr(numgrp=1)
+ / IGMPv3gr(rtype="Mode Is Exclude", maddr="239.1.1.3")
+ )
+
+ self.send(self.pg0, p_j)
+
+ self.assertTrue(
+ wait_for_igmp_event(self, 1, self.pg0, "239.1.1.3", "0.0.0.0", 1)
+ )
+
+ #
+ # A 'allow sources' for {} should be ignored as it should
+ # never be sent.
+ #
+ p_j = (
+ Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+ / IP(
+ src=self.pg0.remote_ip4,
+ dst="224.0.0.22",
+ tos=0xC0,
+ ttl=1,
+ options=[
+ IPOption(
+ copy_flag=1, optclass="control", option="router_alert", length=4
+ )
+ ],
+ )
+ / IGMPv3(type="Version 3 Membership Report")
+ / IGMPv3mr(numgrp=1)
+ / IGMPv3gr(rtype="Allow New Sources", maddr="239.1.1.4")
+ )
+
+ self.send(self.pg0, p_j)
+
+ dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
+ self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.2", "0.0.0.0"))
+ self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.3", "0.0.0.0"))
+ self.assertFalse(find_igmp_state(dump, self.pg0, "239.1.1.4", "0.0.0.0"))
+
+ #
+ # a TO_IN({}) and IS_IN({}) are treated like a (*,G) leave
+ #
+ self.vapi.cli("set logging class igmp level debug")
+ p_l = (
+ Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+ / IP(
+ src=self.pg0.remote_ip4,
+ dst="224.0.0.22",
+ tos=0xC0,
+ ttl=1,
+ options=[
+ IPOption(
+ copy_flag=1, optclass="control", option="router_alert", length=4
+ )
+ ],
+ )
+ / IGMPv3(type="Version 3 Membership Report")
+ / IGMPv3mr(numgrp=1)
+ / IGMPv3gr(rtype="Change To Include Mode", maddr="239.1.1.2")
+ )
+
+ self.send(self.pg0, p_l)
+ self.assertTrue(
+ wait_for_igmp_event(self, 2, self.pg0, "239.1.1.2", "0.0.0.0", 0)
+ )
+
+ p_l = (
+ Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+ / IP(
+ src=self.pg0.remote_ip4,
+ dst="224.0.0.22",
+ tos=0xC0,
+ ttl=1,
+ options=[
+ IPOption(
+ copy_flag=1, optclass="control", option="router_alert", length=4
+ )
+ ],
+ )
+ / IGMPv3(type="Version 3 Membership Report")
+ / IGMPv3mr(numgrp=1)
+ / IGMPv3gr(rtype="Mode Is Include", maddr="239.1.1.3")
+ )
+
+ self.send(self.pg0, p_l)
+
+ self.assertTrue(
+ wait_for_igmp_event(self, 2, self.pg0, "239.1.1.3", "0.0.0.0", 0)
+ )
+ self.assertFalse(self.vapi.igmp_dump(self.pg0.sw_if_index))
+
+ #
+ # disable router config
+ #
+ self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 0, IGMP_MODE.ROUTER)
+
+ def _create_igmpv3_pck(self, itf, rtype, maddr, srcaddrs):
+ p = (
+ Ether(dst=itf.local_mac, src=itf.remote_mac)
+ / IP(
+ src=itf.remote_ip4,
+ dst="224.0.0.22",
+ tos=0xC0,
+ ttl=1,
+ options=[
+ IPOption(
+ copy_flag=1, optclass="control", option="router_alert", length=4
+ )
+ ],
+ )
+ / IGMPv3(type="Version 3 Membership Report")
+ / IGMPv3mr(numgrp=1)
+ / IGMPv3gr(rtype=rtype, maddr=maddr, srcaddrs=srcaddrs)
+ )
+ return p
+
+ def test_igmp_proxy_device(self):
+ """IGMP proxy device"""
+ self.pg2.admin_down()
+ self.pg2.unconfig_ip4()
+ self.pg2.set_table_ip4(0)
+ self.pg2.config_ip4()
+ self.pg2.admin_up()
+
+ self.vapi.cli("test igmp timers query 10 src 3 leave 1")
+
+ # enable IGMP
+ self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 1, IGMP_MODE.HOST)
+ self.vapi.igmp_enable_disable(self.pg1.sw_if_index, 1, IGMP_MODE.ROUTER)
+ self.vapi.igmp_enable_disable(self.pg2.sw_if_index, 1, IGMP_MODE.ROUTER)
+
+ # create IGMP proxy device
+ self.vapi.igmp_proxy_device_add_del(0, self.pg0.sw_if_index, 1)
+ self.vapi.igmp_proxy_device_add_del_interface(0, self.pg1.sw_if_index, 1)
+ self.vapi.igmp_proxy_device_add_del_interface(0, self.pg2.sw_if_index, 1)
+
+ # send join on pg1. join should be proxied by pg0
+ p_j = self._create_igmpv3_pck(
+ self.pg1, "Allow New Sources", "239.1.1.1", ["10.1.1.1", "10.1.1.2"]
+ )
+ self.send(self.pg1, p_j)
+
+ capture = self.pg0.get_capture(1, timeout=1)
+ self.verify_report(
+ capture[0],
+ [
+ IgmpRecord(
+ IgmpSG("239.1.1.1", ["10.1.1.1", "10.1.1.2"]), "Allow New Sources"
+ )
+ ],
+ )
+ self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
+
+ # send join on pg2. join should be proxied by pg0.
+ # the group should contain only 10.1.1.3 as
+ # 10.1.1.1 was already reported
+ p_j = self._create_igmpv3_pck(
+ self.pg2, "Allow New Sources", "239.1.1.1", ["10.1.1.1", "10.1.1.3"]
+ )
+ self.send(self.pg2, p_j)
+
+ capture = self.pg0.get_capture(1, timeout=1)
+ self.verify_report(
+ capture[0],
+ [IgmpRecord(IgmpSG("239.1.1.1", ["10.1.1.3"]), "Allow New Sources")],
+ )
+ self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
+
+ # send leave on pg2. leave for 10.1.1.3 should be proxyed
+ # as pg2 was the only interface interested in 10.1.1.3
+ p_l = self._create_igmpv3_pck(
+ self.pg2, "Block Old Sources", "239.1.1.1", ["10.1.1.3"]
+ )
+ self.send(self.pg2, p_l)
+
+ capture = self.pg0.get_capture(1, timeout=2)
+ self.verify_report(
+ capture[0],
+ [IgmpRecord(IgmpSG("239.1.1.1", ["10.1.1.3"]), "Block Old Sources")],
+ )
+ self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
+
+ # disable igmp on pg1 (also removes interface from proxy device)
+ # proxy leave for 10.1.1.2. pg2 is still interested in 10.1.1.1
+ self.pg_enable_capture(self.pg_interfaces)
+ self.vapi.igmp_enable_disable(self.pg1.sw_if_index, 0, IGMP_MODE.ROUTER)
+
+ capture = self.pg0.get_capture(1, timeout=1)
+ self.verify_report(
+ capture[0],
+ [IgmpRecord(IgmpSG("239.1.1.1", ["10.1.1.2"]), "Block Old Sources")],
+ )
+ self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
+
+ # disable IGMP on pg0 and pg1.
+ # disabling IGMP on pg0 (proxy device upstream interface)
+ # removes this proxy device
+ self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 0, IGMP_MODE.HOST)
+ self.vapi.igmp_enable_disable(self.pg2.sw_if_index, 0, IGMP_MODE.ROUTER)
+ self.assertFalse(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))