IGMP: proxy device
[vpp.git] / test / test_igmp.py
index 22b6d2f..da2fa9a 100644 (file)
@@ -1,16 +1,20 @@
 #!/usr/bin/env python
 
 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 vpp_ip_route import find_mroute, VppIpTable
+
+
+class IgmpMode:
+    HOST = 1
+    ROUTER = 0
 
 
 class TestIgmp(VppTestCase):
@@ -19,11 +23,16 @@ class TestIgmp(VppTestCase):
     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 +42,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,271 +51,692 @@ 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, ttl=1,
-                     options=IPOption(copy_flag=1, optclass=0,
-                                      option="router_alert",
-                                      length=2, value=0)) /
-                  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.assertFalse(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.assertFalse(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
         #
-        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"))
 
-        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"]))
 
-        # 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"]))
+        self.send(self.pg0, p_gs1)
 
-        self.send(self.pg0, p_mr1)
+        capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(capture[0],
+                           [IgmpRecord(h1.sg, "Mode Is Include")])
 
-        # 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.verify_general_query(capture[0])
+        self.send_and_assert_no_replies(self.pg0, p_gs2, timeout=10)
 
-        # 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"]))
+        #
+        # 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_mr2)
+        self.send(self.pg0, p_gs3)
 
-        # wait for VPP to send general query
-        capture = self.pg0.get_capture(1, timeout=61)
-        self.verify_general_query(capture[0])
+        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"]))
+        #
+        # Two source and group specific queires in qucik sucession, 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")])
 
-        self.send(self.pg0, p_l)
+        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")])
+
+        #
+        # remove state, expect the report for the removal
+        #
+        self.remove_group(h1)
+
+        dump = self.vapi.igmp_dump()
+        self.assertFalse(dump)
+
+        #
+        # 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))
+        #
+        # Send a general query (to the all router's address)
+        # expect VPP to respond with a membership report will all sources
+        #
+        self.send(self.pg0, p_g)
+
+        capture = self.pg0.get_capture(1, timeout=10)
+        self.verify_report(capture[0],
+                           [IgmpRecord(h2.sg, "Mode Is Include")])
+
+        #
+        # Group and source specific query; some present some not
+        #
+        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"]))
 
-        # FIXME BUG
+        self.send(self.pg0, p_gs)
+
+        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")])
+
+        #
+        # add loads more groups
+        #
+        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"]))
+
+        #
+        # 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")])
+
+        #
+        # 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)
+
+        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))
+
+        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")])
+
+        #
+        # drop the MTU further (so a 128 sized group won't fit)
+        #
+        self.vapi.sw_interface_set_mtu(self.pg0.sw_if_index, [512, 0, 0, 0])
+
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+
+        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)
+
+        #
+        # 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') /
-               IGMPv3() /
+               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=2, maddr="224.1.1.1", srcaddrs=["10.1.1.1"]))
-        self.send(self.pg0, p_l)
+               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())
 
         #
-        # host has left all groups, no state left.
+        # drop the default timer values so these tests execute in a
+        # reasonable time frame
         #
-        self.sleep(10)
-        self.logger.error(self.vapi.cli("sh igmp config"))
-        self.assertFalse(self.vapi.igmp_dump())
+        self.vapi.cli("test igmp timers query 1 src 3 leave 1")
 
-    def test_igmp_query_resp(self):
-        """ IGMP General Query response """
+        #
+        # 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)
 
         #
-        # VPP acting as a host.
-        #  Add a listener in VPP for (10.1.1.1,244.1.1.1)
+        # wait for router to send general query
         #
-        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()
+        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()
 
-        # verify state exists
-        self.assertTrue(self.vapi.igmp_dump(self.pg0.sw_if_index))
+        #
+        # 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())
 
         #
-        # Send a general query (from a router)
+        # resend the join. wait for two queries and then send a current-state
+        # record to include all sources. this should reset the exiry 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.
         #
-        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"))
+        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)
+        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"))
 
         #
-        # expect VPP to respond with a membership report for the
-        # (10.1.1.1, 224.1.1.1) state
+        # wait for the per-source timer to expire
+        # the state should be reaped
         #
-        capture = self.pg0.get_capture(1, timeout=10)
+        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())
 
-        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")
+        #
+        # resend the join, then a leave. Router sends a gruop+source
+        # specific query containing both sources
+        #
+        self.send(self.pg0, p_j)
 
-    def test_igmp_listen(self):
-        """ IGMP listen (S,G)s """
+        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"])
 
         #
-        # VPP acts as a host
-        #  Add IGMP group state to multiple interfaces and validate its
-        #  presence
+        # the group specific query drops the timeout to leave (=1) seconds
         #
-        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.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())
 
-        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()
+        #
+        # A (*,G) host report
+        #
+        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.2"))
 
-        dump = self.vapi.igmp_dump()
-        self.assertFalse(dump)
+        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))
+
+        #
+        # 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__':