From 947ea6222dad1ef04595c34273e9231395aef443 Mon Sep 17 00:00:00 2001 From: Neale Ranns Date: Thu, 7 Jun 2018 23:48:20 -0700 Subject: IGMP improvements - Enable/Disable an interface for IGMP - improve logging - refactor common code - no orphaned timers - IGMP state changes in main thread only - Large groups split over multiple state-change reports - SSM range configuration API. - more tests Change-Id: If5674f1044e7e97274a711f47807c9ba689d7b9a Signed-off-by: Neale Ranns --- test/framework.py | 5 +- test/test_igmp.py | 732 +++++++++++++++++++++++++++++++++------------- test/vpp_igmp.py | 75 +++-- test/vpp_ip_route.py | 38 ++- test/vpp_papi_provider.py | 35 ++- 5 files changed, 642 insertions(+), 243 deletions(-) (limited to 'test') diff --git a/test/framework.py b/test/framework.py index fdaba2b84d9..4ecb66fe408 100644 --- a/test/framework.py +++ b/test/framework.py @@ -837,12 +837,13 @@ class VppTestCase(unittest.TestCase): "Finished sleep (%s) - slept %ss (wanted %ss)" % ( remark, after - before, timeout)) - def send_and_assert_no_replies(self, intf, pkts, remark=""): + def send_and_assert_no_replies(self, intf, pkts, remark="", timeout=None): self.vapi.cli("clear trace") intf.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - timeout = 1 + if not timeout: + timeout = 1 for i in self.pg_interfaces: i.get_capture(0, timeout=timeout) i.assert_nothing_captured(remark=remark) diff --git a/test/test_igmp.py b/test/test_igmp.py index 22b6d2f02e3..cfdd1a8aed3 100644 --- a/test/test_igmp.py +++ b/test/test_igmp.py @@ -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,589 @@ 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")) - @unittest.skipUnless(running_extended_tests(), "part of extended tests") - def test_igmp_src_exp(self): - """ IGMP per source timer """ + 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")]) # - # 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")]) - # 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) + # + # remove state, expect the report for the removal + # + self.remove_group(h1) + + dump = self.vapi.igmp_dump() + self.assertFalse(dump) # - # host has left all groups, no state left. + # A group with multiple sources # - self.sleep(10) - self.logger.error(self.vapi.cli("sh igmp config")) - self.assertFalse(self.vapi.igmp_dump()) + 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) - 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"])) - # verify state exists - self.assertTrue(self.vapi.igmp_dump(self.pg0.sw_if_index)) + 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")]) # - # 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")]) - 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") + # + # 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")]) - def test_igmp_listen(self): - """ IGMP listen (S,G)s """ + # + # 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) # - # VPP acts as a host - # Add IGMP group state to multiple interfaces and validate its - # presence + # remove state, expect the report for the removal + # the dump should be empty # - 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, [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) - 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.logger.info(self.vapi.cli("sh igmp config")) + self.assertFalse(self.vapi.igmp_dump()) - dump = self.vapi.igmp_dump() - self.assertFalse(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 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. + # + 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 gruop+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()) + + # + # disable router config + # + self.vapi.igmp_enable_disable(self.pg0.sw_if_index, + 0, + IGMP_MODE.ROUTER) if __name__ == '__main__': diff --git a/test/vpp_igmp.py b/test/vpp_igmp.py index d1a308878c5..8f0191644bd 100644 --- a/test/vpp_igmp.py +++ b/test/vpp_igmp.py @@ -1,39 +1,80 @@ from vpp_object import VppObject +import socket + + +class IGMP_MODE: + ROUTER = 0 + HOST = 1 + + +class IGMP_FILTER: + INCLUDE = 1 + EXCLUDE = 0 + + +def find_igmp_state(states, itf, gaddr, saddr): + for s in states: + if s.sw_if_index == itf.sw_if_index and \ + s.gaddr.address == socket.inet_pton(socket.AF_INET, gaddr) and \ + s.saddr.address == socket.inet_pton(socket.AF_INET, saddr): + return True + return False + + +def wait_for_igmp_event(test, timeout, itf, gaddr, saddr, ff): + ev = test.vapi.wait_for_event(timeout, "igmp_event") + if ev.sw_if_index == itf.sw_if_index and \ + ev.gaddr.address == socket.inet_pton(socket.AF_INET, gaddr) and \ + ev.saddr.address == socket.inet_pton(socket.AF_INET, saddr) and \ + ev.filter == ff: + return True + return False class IgmpSG(): - def __init__(self, saddr, gaddr): - self.saddr = saddr + def __init__(self, gaddr, saddrs): self.gaddr = gaddr + self.gaddr_p = socket.inet_pton(socket.AF_INET, gaddr) + self.saddrs = saddrs + self.saddrs_p = [] + self.saddrs_encoded = [] + for s in saddrs: + ss = socket.inet_pton(socket.AF_INET, s) + self.saddrs_p.append(ss) + self.saddrs_encoded.append({'address': ss}) + +class IgmpRecord(): + def __init__(self, sg, type): + self.sg = sg + self.type = type -class VppIgmpConfig(VppObject): - def __init__(self, test, sw_if_index, sg=None): + +class VppHostState(VppObject): + def __init__(self, test, filter, sw_if_index, sg): self._test = test self.sw_if_index = sw_if_index - if isinstance(sg, list): - self.sg_list = sg - else: - self.sg_list = [] - self.sg_list.append(sg) - - def add_sg(self, sg): - self.sg.append(sg) + self.filter = filter + self.sg = sg def add_vpp_config(self): - for e in self.sg_list: - self._test.vapi.igmp_listen( - 1, self.sw_if_index, e.saddr, e.gaddr) + self._test.vapi.igmp_listen( + self.filter, self.sw_if_index, + self.sg.saddrs_encoded, self.sg.gaddr_p) def remove_vpp_config(self): - self._test.vapi.igmp_clear_interface(self.sw_if_index) + self._test.vapi.igmp_listen( + self.filter, + self.sw_if_index, + [], + self.sg.gaddr_p) def __str__(self): return self.object_id() def object_id(self): - return "%s:%d" % (self.sg_list, self.sw_if_index) + return "%s:%d" % (self.sg, self.sw_if_index) def query_vpp_config(self): return self._test.vapi.igmp_dump() diff --git a/test/vpp_ip_route.py b/test/vpp_ip_route.py index 17a42fec706..9e5c53184e3 100644 --- a/test/vpp_ip_route.py +++ b/test/vpp_ip_route.py @@ -60,6 +60,25 @@ def find_route(test, ip_addr, len, table_id=0, inet=AF_INET): return False +def find_mroute(test, grp_addr, src_addr, grp_addr_len, + table_id=0, inet=AF_INET): + if inet == AF_INET: + s = 4 + routes = test.vapi.ip_mfib_dump() + else: + s = 16 + routes = test.vapi.ip6_mfib_dump() + gaddr = inet_pton(inet, grp_addr) + saddr = inet_pton(inet, src_addr) + for e in routes: + if gaddr == e.grp_address[:s] \ + and grp_addr_len == e.address_length \ + and saddr == e.src_address[:s] \ + and table_id == e.table_id: + return True + return False + + class VppIpTable(VppObject): def __init__(self, @@ -324,6 +343,8 @@ class VppIpMRoute(VppObject): self.is_ip6 = is_ip6 self.rpf_id = rpf_id + self.grp_addr_p = grp_addr + self.src_addr_p = src_addr if is_ip6: self.grp_addr = inet_pton(AF_INET6, grp_addr) self.src_addr = inet_pton(AF_INET6, src_addr) @@ -406,17 +427,12 @@ class VppIpMRoute(VppObject): is_ipv6=self.is_ip6) def query_vpp_config(self): - if self.is_ip6: - dump = self._test.vapi.ip6_mfib_dump() - else: - dump = self._test.vapi.ip_mfib_dump() - for e in dump: - if self.grp_addr == e.grp_address \ - and self.grp_addr_len == e.address_length \ - and self.src_addr == e.src_address \ - and self.table_id == e.table_id: - return True - return False + return find_mroute(self._test, + self.grp_addr_p, + self.src_addr_p, + self.grp_addr_len, + self.table_id, + inet=AF_INET6 if self.is_ip6 == 1 else AF_INET) def __str__(self): return self.object_id() diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index 2d4b44764f3..5383b07fc4c 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -3474,7 +3474,14 @@ class VppPapiProvider(object): 'input_source': input_source, 'enable': enable}) - def igmp_listen(self, enable, sw_if_index, saddr, gaddr): + def igmp_enable_disable(self, sw_if_index, enable, host): + """ Enable/disable IGMP on a given interface """ + return self.api(self.papi.igmp_enable_disable, + {'enable': enable, + 'mode': host, + 'sw_if_index': sw_if_index}) + + def igmp_listen(self, filter, sw_if_index, saddrs, gaddr): """ Listen for new (S,G) on specified interface :param enable: add/del @@ -3483,20 +3490,26 @@ class VppPapiProvider(object): :param gaddr: group ip4 addr """ return self.api(self.papi.igmp_listen, - {'enable': enable, - 'sw_if_index': sw_if_index, - 'saddr': saddr, - 'gaddr': gaddr}) + { + 'group': + { + 'filter': filter, + 'sw_if_index': sw_if_index, + 'n_srcs': len(saddrs), + 'saddrs': saddrs, + 'gaddr': + { + 'address': gaddr + } + } + }) def igmp_dump(self, sw_if_index=None): """ Dump all (S,G) interface configurations """ if sw_if_index is None: - dump_all = 1 - sw_if_index = 0 - else: - dump_all = 0 - return self.api(self.papi.igmp_dump, {'sw_if_index': sw_if_index, - 'dump_all': dump_all}) + sw_if_index = 0xffffffff + return self.api(self.papi.igmp_dump, + {'sw_if_index': sw_if_index}) def igmp_clear_interface(self, sw_if_index): """ Remove all (S,G)s from specified interface -- cgit 1.2.3-korg