X-Git-Url: https://gerrit.fd.io/r/gitweb?a=blobdiff_plain;f=test%2Ftest_igmp.py;fp=test%2Ftest_igmp.py;h=8053bc3d544f97a1d7ae7f513cba86adc0aa0455;hb=eddd8e3588561039985b27edf059db6033bfdfab;hp=0000000000000000000000000000000000000000;hpb=fd77f8c00c8e9d528d91a9cefae1878e383582ed;p=vpp.git diff --git a/test/test_igmp.py b/test/test_igmp.py new file mode 100644 index 00000000000..8053bc3d544 --- /dev/null +++ b/test/test_igmp.py @@ -0,0 +1,837 @@ +#!/usr/bin/env python3 + +import unittest + +from scapy.layers.l2 import Ether +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, running_extended_tests +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 """ + + @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(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() + pg.resolve_arp() + + def tearDown(self): + 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() + + def send(self, ti, pkts): + ti.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + def test_igmp_flush(self): + """ IGMP Link Up/down and Flush """ + + # + # FIX THIS. Link down. + # + + def test_igmp_enable(self): + """ IGMP enable/disable on an interface + + check for the addition/removal of the IGMP mroutes """ + + 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) + + 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)) + + 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) + + 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 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 """ + + # + # Enable interface for host functions + # + self.vapi.igmp_enable_disable(self.pg0.sw_if_index, + 1, + IGMP_MODE.HOST) + + # + # 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")) + + # + # 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. + # + 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.send(self.pg0, p_g) + + capture = self.pg0.get_capture(1, timeout=10) + self.verify_report(capture[0], + [IgmpRecord(h1.sg, "Mode Is Include")]) + + # + # Group specific query + # + 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) + + capture = self.pg0.get_capture(1, timeout=10) + self.verify_report(capture[0], + [IgmpRecord(h1.sg, "Mode Is Include")]) + + # + # 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) + + capture = self.pg0.get_capture(1, timeout=10) + self.verify_report(capture[0], + [IgmpRecord(h1.sg, "Mode Is Include")]) + + # + # 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) + + # + # 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) + + # + # 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) + + capture = self.pg0.get_capture(1, timeout=10) + self.verify_report(capture[0], + [IgmpRecord(h1.sg, "Mode Is Include")]) + + # + # 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")]) + + 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"])) + + 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) + # 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__': + unittest.main(testRunner=VppTestRunner)