tests: replace pycodestyle with black
[vpp.git] / test / test_igmp.py
index e741e6b..6e9defd 100644 (file)
@@ -1,29 +1,56 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 import unittest
-import socket
 
-from framework import VppTestCase, VppTestRunner, running_extended_tests
-from vpp_igmp import *
-
-from scapy.packet import Raw
 from scapy.layers.l2 import Ether
-from scapy.layers.inet import IP
-from scapy.contrib.igmpv3 import *
-from scapy.contrib.igmp import *
+from scapy.packet import Raw
+from scapy.layers.inet import IP, IPOption
+from scapy.contrib.igmpv3 import IGMPv3, IGMPv3gr, IGMPv3mq, IGMPv3mr
+
+from framework import tag_fixme_vpp_workers
+from framework import VppTestCase, VppTestRunner
+from vpp_igmp import (
+    find_igmp_state,
+    IGMP_FILTER,
+    IgmpRecord,
+    IGMP_MODE,
+    IgmpSG,
+    VppHostState,
+    wait_for_igmp_event,
+)
+from vpp_ip_route import find_mroute, VppIpTable
+
+
+class IgmpMode:
+    HOST = 1
+    ROUTER = 0
 
 
+@tag_fixme_vpp_workers
 class TestIgmp(VppTestCase):
-    """ IGMP Test Case """
+    """IGMP Test Case"""
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestIgmp, cls).setUpClass()
+
+    @classmethod
+    def tearDownClass(cls):
+        super(TestIgmp, cls).tearDownClass()
 
     def setUp(self):
         super(TestIgmp, self).setUp()
 
-        self.create_pg_interfaces(range(2))
+        self.create_pg_interfaces(range(4))
         self.sg_list = []
         self.config_list = []
 
         self.ip_addr = []
+        self.ip_table = VppIpTable(self, 1)
+        self.ip_table.add_vpp_config()
+
+        for pg in self.pg_interfaces[2:]:
+            pg.set_table_ip4(1)
         for pg in self.pg_interfaces:
             pg.admin_up()
             pg.config_ip4()
@@ -33,6 +60,7 @@ class TestIgmp(VppTestCase):
         for pg in self.pg_interfaces:
             self.vapi.igmp_clear_interface(pg.sw_if_index)
             pg.unconfig_ip4()
+            pg.set_table_ip4(0)
             pg.admin_down()
         super(TestIgmp, self).tearDown()
 
@@ -41,268 +69,925 @@ class TestIgmp(VppTestCase):
         self.pg_enable_capture(self.pg_interfaces)
         self.pg_start()
 
-    def test_igmp_parse_report(self):
-        """ IGMP parse Membership Report """
+    def test_igmp_flush(self):
+        """IGMP Link Up/down and Flush"""
 
         #
-        # VPP acts as a router
+        # FIX THIS. Link down.
         #
-        self.vapi.want_igmp_events(1)
 
-        # hos sends join IGMP 'join'
-        p_join = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
-                  IP(src=self.pg0.remote_ip4, dst='224.0.0.22', tos=0xc0) /
-                  IGMPv3() /
-                  IGMPv3mr(numgrp=1) /
-                  IGMPv3gr(rtype=3, maddr="224.1.1.1", srcaddrs=["10.1.1.1"]))
+    def test_igmp_enable(self):
+        """IGMP enable/disable on an interface
 
-        self.send(self.pg0, p_join)
+        check for the addition/removal of the IGMP mroutes"""
 
-        # search for the corresponding state created in VPP
-        dump = self.vapi.igmp_dump()
-        self.assertEqual(len(dump), 1)
-        self.assertEqual(dump[0].sw_if_index, self.pg0.sw_if_index)
-        self.assertEqual(dump[0].gaddr,
-                         socket.inet_pton(socket.AF_INET,
-                                          "224.1.1.1"))
-        self.assertEqual(dump[0].saddr,
-                         socket.inet_pton(socket.AF_INET,
-                                          "10.1.1.1"))
+        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.HOST)
 
-        # VPP sends a notification that a new group has been joined
-        ev = self.vapi.wait_for_event(2, "igmp_event")
-
-        self.assertEqual(ev.saddr,
-                         socket.inet_pton(socket.AF_INET,
-                                          "10.1.1.1"))
-        self.assertEqual(ev.gaddr,
-                         socket.inet_pton(socket.AF_INET,
-                                          "224.1.1.1"))
-        self.assertEqual(ev.is_join, 1)
-
-        # host sends IGMP leave
-        p_leave = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
-                   IP(src=self.pg0.remote_ip4, dst='224.0.0.22', tos=0xc0) /
-                   IGMPv3() /
-                   IGMPv3mr(numgrp=1) /
-                   IGMPv3gr(rtype=4, maddr="224.1.1.1", srcaddrs=["10.1.1.1"]))
-
-        self.send(self.pg0, p_leave)
-
-        # VPP sends a notification that a new group has been left
-        ev = self.vapi.wait_for_event(2, "igmp_event")
-
-        self.assertEqual(ev.saddr,
-                         socket.inet_pton(socket.AF_INET,
-                                          "10.1.1.1"))
-        self.assertEqual(ev.gaddr,
-                         socket.inet_pton(socket.AF_INET,
-                                          "224.1.1.1"))
-        self.assertEqual(ev.is_join, 0)
-
-        # state gone
-        dump = self.vapi.igmp_dump()
-        self.assertFalse(dump)
+        self.assertTrue(find_mroute(self, "224.0.0.1", "0.0.0.0", 32))
+        self.assertTrue(find_mroute(self, "224.0.0.22", "0.0.0.0", 32))
 
-        # resend the join
-        self.send(self.pg0, p_join)
-        dump = self.vapi.igmp_dump()
-        self.assertEqual(len(dump), 1)
-        self.assertEqual(dump[0].sw_if_index, self.pg0.sw_if_index)
-        self.assertEqual(dump[0].gaddr,
-                         socket.inet_pton(socket.AF_INET,
-                                          "224.1.1.1"))
-        self.assertEqual(dump[0].saddr,
-                         socket.inet_pton(socket.AF_INET,
-                                          "10.1.1.1"))
-
-        # IGMP block
-        p_block = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
-                   IP(src=self.pg0.remote_ip4, dst='224.0.0.22', tos=0xc0) /
-                   IGMPv3() /
-                   IGMPv3mr(numgrp=1) /
-                   IGMPv3gr(rtype=6, maddr="224.1.1.1", srcaddrs=["10.1.1.1"]))
-
-        self.send(self.pg0, p_block)
+        self.vapi.igmp_enable_disable(self.pg2.sw_if_index, 1, IGMP_MODE.HOST)
+        self.vapi.igmp_enable_disable(self.pg3.sw_if_index, 1, IGMP_MODE.HOST)
 
-        dump = self.vapi.igmp_dump()
-        self.assertFalse(dump)
+        self.assertTrue(find_mroute(self, "224.0.0.1", "0.0.0.0", 32, table_id=1))
+        self.assertTrue(find_mroute(self, "224.0.0.22", "0.0.0.0", 32, table_id=1))
+        self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 0, IGMP_MODE.HOST)
+        self.vapi.igmp_enable_disable(self.pg1.sw_if_index, 0, IGMP_MODE.HOST)
+        self.vapi.igmp_enable_disable(self.pg2.sw_if_index, 0, IGMP_MODE.HOST)
+        self.vapi.igmp_enable_disable(self.pg3.sw_if_index, 0, IGMP_MODE.HOST)
+
+        self.assertTrue(find_mroute(self, "224.0.0.1", "0.0.0.0", 32))
+        self.assertFalse(find_mroute(self, "224.0.0.22", "0.0.0.0", 32))
+        self.assertTrue(find_mroute(self, "224.0.0.1", "0.0.0.0", 32, table_id=1))
+        self.assertFalse(find_mroute(self, "224.0.0.22", "0.0.0.0", 32, table_id=1))
 
     def verify_general_query(self, p):
         ip = p[IP]
+        self.assertEqual(len(ip.options), 1)
+        self.assertEqual(ip.options[0].option, 20)
         self.assertEqual(ip.dst, "224.0.0.1")
         self.assertEqual(ip.proto, 2)
         igmp = p[IGMPv3]
         self.assertEqual(igmp.type, 0x11)
         self.assertEqual(igmp.gaddr, "0.0.0.0")
 
-    def test_igmp_send_query(self):
-        """ IGMP send General Query """
+    def verify_group_query(self, p, grp, srcs):
+        ip = p[IP]
+        self.assertEqual(ip.dst, grp)
+        self.assertEqual(ip.proto, 2)
+        self.assertEqual(len(ip.options), 1)
+        self.assertEqual(ip.options[0].option, 20)
+        self.assertEqual(ip.proto, 2)
+        igmp = p[IGMPv3]
+        self.assertEqual(igmp.type, 0x11)
+        self.assertEqual(igmp.gaddr, grp)
+
+    def verify_report(self, rx, records):
+        ip = rx[IP]
+        self.assertEqual(rx[IP].dst, "224.0.0.22")
+        self.assertEqual(len(ip.options), 1)
+        self.assertEqual(ip.options[0].option, 20)
+        self.assertEqual(ip.proto, 2)
+        self.assertEqual(
+            IGMPv3.igmpv3types[rx[IGMPv3].type], "Version 3 Membership Report"
+        )
+        self.assertEqual(rx[IGMPv3mr].numgrp, len(records))
+
+        received = rx[IGMPv3mr].records
+
+        for ii in range(len(records)):
+            gr = received[ii]
+            r = records[ii]
+            self.assertEqual(IGMPv3gr.igmpv3grtypes[gr.rtype], r.type)
+            self.assertEqual(gr.numsrc, len(r.sg.saddrs))
+            self.assertEqual(gr.maddr, r.sg.gaddr)
+            self.assertEqual(len(gr.srcaddrs), len(r.sg.saddrs))
+
+            self.assertEqual(sorted(gr.srcaddrs), sorted(r.sg.saddrs))
+
+    def add_group(self, itf, sg, n_pkts=2):
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+
+        hs = VppHostState(self, IGMP_FILTER.INCLUDE, itf.sw_if_index, sg)
+        hs.add_vpp_config()
+
+        capture = itf.get_capture(n_pkts, timeout=10)
+
+        # reports are transmitted twice due to default rebostness value=2
+        self.verify_report(capture[0], [IgmpRecord(sg, "Allow New Sources")]),
+        self.verify_report(capture[1], [IgmpRecord(sg, "Allow New Sources")]),
+
+        return hs
+
+    def remove_group(self, hs):
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+        hs.remove_vpp_config()
+
+        capture = self.pg0.get_capture(1, timeout=10)
+
+        self.verify_report(capture[0], [IgmpRecord(hs.sg, "Block Old Sources")])
+
+    def test_igmp_host(self):
+        """IGMP Host functions"""
 
         #
-        # VPP acts as a router.
-        #   Send a membership report so VPP builds state
+        # Enable interface for host functions
         #
-        p_mr = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
-                IP(src=self.pg0.remote_ip4, dst='224.0.0.22') /
-                IGMPv3() /
-                IGMPv3mr(numgrp=1) /
-                IGMPv3gr(rtype=3, maddr="224.1.1.1", srcaddrs=["10.1.1.1"]))
+        self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 1, IGMP_MODE.HOST)
 
-        self.send(self.pg0, p_mr)
-        self.logger.info(self.vapi.cli("sh igmp config"))
+        #
+        # Add one S,G of state and expect a state-change event report
+        # indicating the addition of the S,G
+        #
+        h1 = self.add_group(self.pg0, IgmpSG("239.1.1.1", ["1.1.1.1"]))
+
+        # search for the corresponding state created in VPP
+        dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
+        self.assertEqual(len(dump), 1)
+        self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", "1.1.1.1"))
 
         #
-        # wait for VPP to send out the General Query
+        # Send a general query (to the all router's address)
+        # expect VPP to respond with a membership report.
+        # Pad the query with 0 - some devices in the big wild
+        # internet are prone to this.
         #
-        capture = self.pg0.get_capture(1, timeout=61)
+        p_g = (
+            Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+            / IP(src=self.pg0.remote_ip4, dst="224.0.0.1", tos=0xC0)
+            / IGMPv3(type="Membership Query", mrcode=100)
+            / IGMPv3mq(gaddr="0.0.0.0")
+            / Raw(b"\x00" * 10)
+        )
 
-        self.verify_general_query(capture[0])
+        self.send(self.pg0, p_g)
+
+        capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
 
         #
-        # the state will expire in 10 more seconds
+        # Group specific query
         #
-        self.sleep(10)
-        self.assertFalse(self.vapi.igmp_dump())
+        p_gs = (
+            Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+            / IP(
+                src=self.pg0.remote_ip4,
+                dst="239.1.1.1",
+                tos=0xC0,
+                options=[
+                    IPOption(copy_flag=1, optclass="control", option="router_alert")
+                ],
+            )
+            / IGMPv3(type="Membership Query", mrcode=100)
+            / IGMPv3mq(gaddr="239.1.1.1")
+        )
+
+        self.send(self.pg0, p_gs)
 
-    @unittest.skipUnless(running_extended_tests(), "part of extended tests")
-    def test_igmp_src_exp(self):
-        """ IGMP per source timer """
+        capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
 
         #
-        # VPP Acts as a router
+        # A group and source specific query, with the source matching
+        # the source VPP has
         #
+        p_gs1 = (
+            Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+            / IP(
+                src=self.pg0.remote_ip4,
+                dst="239.1.1.1",
+                tos=0xC0,
+                options=[
+                    IPOption(copy_flag=1, optclass="control", option="router_alert")
+                ],
+            )
+            / IGMPv3(type="Membership Query", mrcode=100)
+            / IGMPv3mq(gaddr="239.1.1.1", srcaddrs=["1.1.1.1"])
+        )
+
+        self.send(self.pg0, p_gs1)
 
-        # Host join for (10.1.1.1,224.1.1.1)
-        p_mr1 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
-                 IP(src=self.pg0.remote_ip4, dst='224.0.0.22') /
-                 IGMPv3() /
-                 IGMPv3mr(numgrp=1) /
-                 IGMPv3gr(rtype=3, maddr="224.1.1.1", srcaddrs=["10.1.1.1"]))
+        capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
 
-        self.send(self.pg0, p_mr1)
+        #
+        # A group and source specific query that reports more sources
+        # than the packet actually has.
+        #
+        p_gs2 = (
+            Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+            / IP(
+                src=self.pg0.remote_ip4,
+                dst="239.1.1.1",
+                tos=0xC0,
+                options=[
+                    IPOption(copy_flag=1, optclass="control", option="router_alert")
+                ],
+            )
+            / IGMPv3(type="Membership Query", mrcode=100)
+            / IGMPv3mq(gaddr="239.1.1.1", numsrc=4, srcaddrs=["1.1.1.1"])
+        )
+
+        self.send_and_assert_no_replies(self.pg0, p_gs2, timeout=10)
 
-        # VPP (router) sends General Query
-        capture = self.pg0.get_capture(1, timeout=61)
+        #
+        # A group and source specific query, with the source NOT matching
+        # the source VPP has. There should be no response.
+        #
+        p_gs2 = (
+            Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+            / IP(
+                src=self.pg0.remote_ip4,
+                dst="239.1.1.1",
+                tos=0xC0,
+                options=[
+                    IPOption(copy_flag=1, optclass="control", option="router_alert")
+                ],
+            )
+            / IGMPv3(type="Membership Query", mrcode=100)
+            / IGMPv3mq(gaddr="239.1.1.1", srcaddrs=["1.1.1.2"])
+        )
+
+        self.send_and_assert_no_replies(self.pg0, p_gs2, timeout=10)
 
-        self.verify_general_query(capture[0])
+        #
+        # A group and source specific query, with the multiple sources
+        # one of which matches the source VPP has.
+        # The report should contain only the source VPP has.
+        #
+        p_gs3 = (
+            Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+            / IP(
+                src=self.pg0.remote_ip4,
+                dst="239.1.1.1",
+                tos=0xC0,
+                options=[
+                    IPOption(copy_flag=1, optclass="control", option="router_alert")
+                ],
+            )
+            / IGMPv3(type="Membership Query", mrcode=100)
+            / IGMPv3mq(gaddr="239.1.1.1", srcaddrs=["1.1.1.1", "1.1.1.2", "1.1.1.3"])
+        )
+
+        self.send(self.pg0, p_gs3)
 
-        # host join for same G and another S: (10.1.1.2,224.1.1.1)
-        # therefore leaving (10.1.1.1,224.1.1.1)
-        p_mr2 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
-                 IP(src=self.pg0.remote_ip4, dst='224.0.0.22') /
-                 IGMPv3() /
-                 IGMPv3mr(numgrp=1) /
-                 IGMPv3gr(rtype=2, maddr="224.1.1.1", srcaddrs=["10.1.1.2"]))
+        capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
 
-        self.send(self.pg0, p_mr2)
+        #
+        # Two source and group specific queries in quick succession, the
+        # first does not have VPPs source the second does. then vice-versa
+        #
+        self.send(self.pg0, [p_gs2, p_gs1])
+        capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
 
-        # wait for VPP to send general query
-        capture = self.pg0.get_capture(1, timeout=61)
-        self.verify_general_query(capture[0])
+        self.send(self.pg0, [p_gs1, p_gs2])
+        capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
 
-        # host leaves (10.1.1.2,224.1.1.1)
-        p_l = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
-               IP(src=self.pg0.remote_ip4, dst='224.0.0.22') /
-               IGMPv3() /
-               IGMPv3mr(numgrp=1) /
-               IGMPv3gr(rtype=2, maddr="224.1.1.1", srcaddrs=["10.1.1.2"]))
+        #
+        # remove state, expect the report for the removal
+        #
+        self.remove_group(h1)
 
-        self.send(self.pg0, p_l)
+        dump = self.vapi.igmp_dump()
+        self.assertFalse(dump)
 
-        # FIXME BUG
-        p_l = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
-               IP(src=self.pg0.remote_ip4, dst='224.0.0.22') /
-               IGMPv3() /
-               IGMPv3mr(numgrp=1) /
-               IGMPv3gr(rtype=2, maddr="224.1.1.1", srcaddrs=["10.1.1.1"]))
-        self.send(self.pg0, p_l)
+        #
+        # A group with multiple sources
+        #
+        h2 = self.add_group(
+            self.pg0, IgmpSG("239.1.1.1", ["1.1.1.1", "1.1.1.2", "1.1.1.3"])
+        )
 
+        # search for the corresponding state created in VPP
+        dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
+        self.assertEqual(len(dump), 3)
+        for s in h2.sg.saddrs:
+            self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", s))
         #
-        # host has left all groups, no state left.
+        # Send a general query (to the all router's address)
+        # expect VPP to respond with a membership report will all sources
         #
-        self.sleep(10)
-        self.logger.error(self.vapi.cli("sh igmp config"))
-        self.assertFalse(self.vapi.igmp_dump())
+        self.send(self.pg0, p_g)
 
-    def test_igmp_query_resp(self):
-        """ IGMP General Query response """
+        capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(capture[0], [IgmpRecord(h2.sg, "Mode Is Include")])
 
         #
-        # VPP acting as a host.
-        #  Add a listener in VPP for (10.1.1.1,244.1.1.1)
+        # Group and source specific query; some present some not
         #
-        self.config_list.append(
-            VppIgmpConfig(
-                self, self.pg0.sw_if_index, IgmpSG(
-                    socket.inet_pton(
-                        socket.AF_INET, "10.1.1.1"), socket.inet_pton(
-                        socket.AF_INET, "224.1.1.1"))))
-        self.config_list[0].add_vpp_config()
+        p_gs = (
+            Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
+            / IP(
+                src=self.pg0.remote_ip4,
+                dst="239.1.1.1",
+                tos=0xC0,
+                options=[
+                    IPOption(copy_flag=1, optclass="control", option="router_alert")
+                ],
+            )
+            / IGMPv3(type="Membership Query", mrcode=100)
+            / IGMPv3mq(gaddr="239.1.1.1", srcaddrs=["1.1.1.1", "1.1.1.2", "1.1.1.4"])
+        )
+
+        self.send(self.pg0, p_gs)
 
-        # verify state exists
-        self.assertTrue(self.vapi.igmp_dump(self.pg0.sw_if_index))
+        capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(
+            capture[0],
+            [
+                IgmpRecord(
+                    IgmpSG("239.1.1.1", ["1.1.1.1", "1.1.1.2"]), "Mode Is Include"
+                )
+            ],
+        )
 
         #
-        # Send a general query (from a router)
+        # add loads more groups
         #
-        p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
-             IP(src=self.pg0.remote_ip4, dst='224.0.0.1', tos=0xc0) /
-             IGMPv3(type=0x11, mrcode=100) /
-             IGMPv3mq(gaddr="0.0.0.0"))
+        h3 = self.add_group(
+            self.pg0, IgmpSG("239.1.1.2", ["2.1.1.1", "2.1.1.2", "2.1.1.3"])
+        )
+        h4 = self.add_group(
+            self.pg0, IgmpSG("239.1.1.3", ["3.1.1.1", "3.1.1.2", "3.1.1.3"])
+        )
+        h5 = self.add_group(
+            self.pg0, IgmpSG("239.1.1.4", ["4.1.1.1", "4.1.1.2", "4.1.1.3"])
+        )
+        h6 = self.add_group(
+            self.pg0, IgmpSG("239.1.1.5", ["5.1.1.1", "5.1.1.2", "5.1.1.3"])
+        )
+        h7 = self.add_group(
+            self.pg0,
+            IgmpSG(
+                "239.1.1.6",
+                [
+                    "6.1.1.1",
+                    "6.1.1.2",
+                    "6.1.1.3",
+                    "6.1.1.4",
+                    "6.1.1.5",
+                    "6.1.1.6",
+                    "6.1.1.7",
+                    "6.1.1.8",
+                    "6.1.1.9",
+                    "6.1.1.10",
+                    "6.1.1.11",
+                    "6.1.1.12",
+                    "6.1.1.13",
+                    "6.1.1.14",
+                    "6.1.1.15",
+                    "6.1.1.16",
+                ],
+            ),
+        )
 
-        self.send(self.pg0, p)
+        #
+        # general query.
+        # the order the groups come in is not important, so what is
+        # checked for is what VPP is sending today.
+        #
+        self.send(self.pg0, p_g)
+
+        capture = self.pg0.get_capture(1, timeout=10)
+
+        self.verify_report(
+            capture[0],
+            [
+                IgmpRecord(h3.sg, "Mode Is Include"),
+                IgmpRecord(h2.sg, "Mode Is Include"),
+                IgmpRecord(h6.sg, "Mode Is Include"),
+                IgmpRecord(h4.sg, "Mode Is Include"),
+                IgmpRecord(h5.sg, "Mode Is Include"),
+                IgmpRecord(h7.sg, "Mode Is Include"),
+            ],
+        )
 
         #
-        # expect VPP to respond with a membership report for the
-        # (10.1.1.1, 224.1.1.1) state
+        # modify a group to add and remove some sources
         #
+        h7.sg = IgmpSG(
+            "239.1.1.6",
+            [
+                "6.1.1.1",
+                "6.1.1.2",
+                "6.1.1.5",
+                "6.1.1.6",
+                "6.1.1.7",
+                "6.1.1.8",
+                "6.1.1.9",
+                "6.1.1.10",
+                "6.1.1.11",
+                "6.1.1.12",
+                "6.1.1.13",
+                "6.1.1.14",
+                "6.1.1.15",
+                "6.1.1.16",
+                "6.1.1.17",
+                "6.1.1.18",
+            ],
+        )
+
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+        h7.add_vpp_config()
+
         capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(
+            capture[0],
+            [
+                IgmpRecord(
+                    IgmpSG("239.1.1.6", ["6.1.1.17", "6.1.1.18"]), "Allow New Sources"
+                ),
+                IgmpRecord(
+                    IgmpSG("239.1.1.6", ["6.1.1.3", "6.1.1.4"]), "Block Old Sources"
+                ),
+            ],
+        )
+
+        #
+        # add an additional groups with many sources so that each group
+        # consumes the link MTU. We should therefore see multiple state
+        # state reports when queried.
+        #
+        self.vapi.sw_interface_set_mtu(self.pg0.sw_if_index, [560, 0, 0, 0])
+
+        src_list = []
+        for i in range(128):
+            src_list.append("10.1.1.%d" % i)
 
-        self.assertEqual(capture[0][IGMPv3].type, 0x22)
-        self.assertEqual(capture[0][IGMPv3mr].numgrp, 1)
-        self.assertEqual(capture[0][IGMPv3gr].rtype, 1)
-        self.assertEqual(capture[0][IGMPv3gr].numsrc, 1)
-        self.assertEqual(capture[0][IGMPv3gr].maddr, "224.1.1.1")
-        self.assertEqual(len(capture[0][IGMPv3gr].srcaddrs), 1)
-        self.assertEqual(capture[0][IGMPv3gr].srcaddrs[0], "10.1.1.1")
+        h8 = self.add_group(self.pg0, IgmpSG("238.1.1.1", src_list))
+        h9 = self.add_group(self.pg0, IgmpSG("238.1.1.2", src_list))
 
-    def test_igmp_listen(self):
-        """ IGMP listen (S,G)s """
+        self.send(self.pg0, p_g)
+
+        capture = self.pg0.get_capture(4, timeout=10)
+
+        self.verify_report(
+            capture[0],
+            [
+                IgmpRecord(h3.sg, "Mode Is Include"),
+                IgmpRecord(h2.sg, "Mode Is Include"),
+                IgmpRecord(h6.sg, "Mode Is Include"),
+                IgmpRecord(h4.sg, "Mode Is Include"),
+                IgmpRecord(h5.sg, "Mode Is Include"),
+            ],
+        )
+        self.verify_report(capture[1], [IgmpRecord(h8.sg, "Mode Is Include")])
+        self.verify_report(capture[2], [IgmpRecord(h7.sg, "Mode Is Include")])
+        self.verify_report(capture[3], [IgmpRecord(h9.sg, "Mode Is Include")])
 
         #
-        # VPP acts as a host
-        #  Add IGMP group state to multiple interfaces and validate its
-        #  presence
+        # drop the MTU further (so a 128 sized group won't fit)
         #
-        for pg in self.pg_interfaces:
-            self.sg_list.append(IgmpSG(socket.inet_pton(
-                socket.AF_INET, "10.1.1.%d" % pg._sw_if_index),
-                socket.inet_pton(socket.AF_INET, "224.1.1.1")))
+        self.vapi.sw_interface_set_mtu(self.pg0.sw_if_index, [512, 0, 0, 0])
 
-        for pg in self.pg_interfaces:
-            self.config_list.append(
-                VppIgmpConfig(
-                    self,
-                    pg._sw_if_index,
-                    self.sg_list))
-            self.config_list[-1].add_vpp_config()
-
-        for config in self.config_list:
-            dump = self.vapi.igmp_dump(config.sw_if_index)
-            self.assertTrue(dump)
-            self.assertEqual(len(dump), len(config.sg_list))
-            for idx, e in enumerate(dump):
-                self.assertEqual(e.sw_if_index, config.sw_if_index)
-                self.assertEqual(e.saddr, config.sg_list[idx].saddr)
-                self.assertEqual(e.gaddr, config.sg_list[idx].gaddr)
-
-        for config in self.config_list:
-            config.remove_vpp_config()
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
 
-        dump = self.vapi.igmp_dump()
-        self.assertFalse(dump)
+        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")
+                ],
+            )
+            / 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")
+                ],
+            )
+            / 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")
+                ],
+            )
+            / 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")
+                ],
+            )
+            / 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")
+                ],
+            )
+            / 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")
+                ],
+            )
+            / 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")
+                ],
+            )
+            / 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")
+                ],
+            )
+            / 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")
+                ],
+            )
+            / 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))
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main(testRunner=VppTestRunner)