Tests Cleanup: Fix missing calls to setUpClass/tearDownClass.
[vpp.git] / test / test_igmp.py
1 #!/usr/bin/env python
2
3 import unittest
4
5 from scapy.layers.l2 import Ether
6 from scapy.layers.inet import IP, IPOption
7 from scapy.contrib.igmpv3 import IGMPv3, IGMPv3gr, IGMPv3mq, IGMPv3mr
8
9 from framework import VppTestCase, VppTestRunner, running_extended_tests
10 from vpp_igmp import find_igmp_state, IGMP_FILTER, IgmpRecord, IGMP_MODE, \
11     IgmpSG, VppHostState, wait_for_igmp_event
12 from vpp_ip_route import find_mroute, VppIpTable
13
14
15 class IgmpMode:
16     HOST = 1
17     ROUTER = 0
18
19
20 class TestIgmp(VppTestCase):
21     """ IGMP Test Case """
22
23     @classmethod
24     def setUpClass(cls):
25         super(TestIgmp, cls).setUpClass()
26
27     @classmethod
28     def tearDownClass(cls):
29         super(TestIgmp, cls).tearDownClass()
30
31     def setUp(self):
32         super(TestIgmp, self).setUp()
33
34         self.create_pg_interfaces(range(4))
35         self.sg_list = []
36         self.config_list = []
37
38         self.ip_addr = []
39         self.ip_table = VppIpTable(self, 1)
40         self.ip_table.add_vpp_config()
41
42         for pg in self.pg_interfaces[2:]:
43             pg.set_table_ip4(1)
44         for pg in self.pg_interfaces:
45             pg.admin_up()
46             pg.config_ip4()
47             pg.resolve_arp()
48
49     def tearDown(self):
50         for pg in self.pg_interfaces:
51             self.vapi.igmp_clear_interface(pg.sw_if_index)
52             pg.unconfig_ip4()
53             pg.set_table_ip4(0)
54             pg.admin_down()
55         super(TestIgmp, self).tearDown()
56
57     def send(self, ti, pkts):
58         ti.add_stream(pkts)
59         self.pg_enable_capture(self.pg_interfaces)
60         self.pg_start()
61
62     def test_igmp_flush(self):
63         """ IGMP Link Up/down and Flush """
64
65         #
66         # FIX THIS. Link down.
67         #
68
69     def test_igmp_enable(self):
70         """ IGMP enable/disable on an interface
71
72         check for the addition/removal of the IGMP mroutes """
73
74         self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 1, IGMP_MODE.HOST)
75         self.vapi.igmp_enable_disable(self.pg1.sw_if_index, 1, IGMP_MODE.HOST)
76
77         self.assertTrue(find_mroute(self, "224.0.0.1", "0.0.0.0", 32))
78         self.assertTrue(find_mroute(self, "224.0.0.22", "0.0.0.0", 32))
79
80         self.vapi.igmp_enable_disable(self.pg2.sw_if_index, 1, IGMP_MODE.HOST)
81         self.vapi.igmp_enable_disable(self.pg3.sw_if_index, 1, IGMP_MODE.HOST)
82
83         self.assertTrue(find_mroute(self, "224.0.0.1", "0.0.0.0", 32,
84                                     table_id=1))
85         self.assertTrue(find_mroute(self, "224.0.0.22", "0.0.0.0", 32,
86                                     table_id=1))
87         self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 0, IGMP_MODE.HOST)
88         self.vapi.igmp_enable_disable(self.pg1.sw_if_index, 0, IGMP_MODE.HOST)
89         self.vapi.igmp_enable_disable(self.pg2.sw_if_index, 0, IGMP_MODE.HOST)
90         self.vapi.igmp_enable_disable(self.pg3.sw_if_index, 0, IGMP_MODE.HOST)
91
92         self.assertFalse(find_mroute(self, "224.0.0.1", "0.0.0.0", 32))
93         self.assertFalse(find_mroute(self, "224.0.0.22", "0.0.0.0", 32))
94         self.assertFalse(find_mroute(self, "224.0.0.1", "0.0.0.0", 32,
95                                      table_id=1))
96         self.assertFalse(find_mroute(self, "224.0.0.22", "0.0.0.0", 32,
97                                      table_id=1))
98
99     def verify_general_query(self, p):
100         ip = p[IP]
101         self.assertEqual(len(ip.options), 1)
102         self.assertEqual(ip.options[0].option, 20)
103         self.assertEqual(ip.dst, "224.0.0.1")
104         self.assertEqual(ip.proto, 2)
105         igmp = p[IGMPv3]
106         self.assertEqual(igmp.type, 0x11)
107         self.assertEqual(igmp.gaddr, "0.0.0.0")
108
109     def verify_group_query(self, p, grp, srcs):
110         ip = p[IP]
111         self.assertEqual(ip.dst, grp)
112         self.assertEqual(ip.proto, 2)
113         self.assertEqual(len(ip.options), 1)
114         self.assertEqual(ip.options[0].option, 20)
115         self.assertEqual(ip.proto, 2)
116         igmp = p[IGMPv3]
117         self.assertEqual(igmp.type, 0x11)
118         self.assertEqual(igmp.gaddr, grp)
119
120     def verify_report(self, rx, records):
121         ip = rx[IP]
122         self.assertEqual(rx[IP].dst, "224.0.0.22")
123         self.assertEqual(len(ip.options), 1)
124         self.assertEqual(ip.options[0].option, 20)
125         self.assertEqual(ip.proto, 2)
126         self.assertEqual(IGMPv3.igmpv3types[rx[IGMPv3].type],
127                          "Version 3 Membership Report")
128         self.assertEqual(rx[IGMPv3mr].numgrp, len(records))
129
130         received = rx[IGMPv3mr].records
131
132         for ii in range(len(records)):
133             gr = received[ii]
134             r = records[ii]
135             self.assertEqual(IGMPv3gr.igmpv3grtypes[gr.rtype], r.type)
136             self.assertEqual(gr.numsrc, len(r.sg.saddrs))
137             self.assertEqual(gr.maddr, r.sg.gaddr)
138             self.assertEqual(len(gr.srcaddrs), len(r.sg.saddrs))
139
140             self.assertEqual(sorted(gr.srcaddrs),
141                              sorted(r.sg.saddrs))
142
143     def add_group(self, itf, sg, n_pkts=2):
144         self.pg_enable_capture(self.pg_interfaces)
145         self.pg_start()
146
147         hs = VppHostState(self,
148                           IGMP_FILTER.INCLUDE,
149                           itf.sw_if_index,
150                           sg)
151         hs.add_vpp_config()
152
153         capture = itf.get_capture(n_pkts, timeout=10)
154
155         # reports are transmitted twice due to default rebostness value=2
156         self.verify_report(capture[0],
157                            [IgmpRecord(sg, "Allow New Sources")]),
158         self.verify_report(capture[1],
159                            [IgmpRecord(sg, "Allow New Sources")]),
160
161         return hs
162
163     def remove_group(self, hs):
164         self.pg_enable_capture(self.pg_interfaces)
165         self.pg_start()
166         hs.remove_vpp_config()
167
168         capture = self.pg0.get_capture(1, timeout=10)
169
170         self.verify_report(capture[0],
171                            [IgmpRecord(hs.sg, "Block Old Sources")])
172
173     def test_igmp_host(self):
174         """ IGMP Host functions """
175
176         #
177         # Enable interface for host functions
178         #
179         self.vapi.igmp_enable_disable(self.pg0.sw_if_index,
180                                       1,
181                                       IGMP_MODE.HOST)
182
183         #
184         # Add one S,G of state and expect a state-change event report
185         # indicating the addition of the S,G
186         #
187         h1 = self.add_group(self.pg0, IgmpSG("239.1.1.1", ["1.1.1.1"]))
188
189         # search for the corresponding state created in VPP
190         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
191         self.assertEqual(len(dump), 1)
192         self.assertTrue(find_igmp_state(dump, self.pg0,
193                                         "239.1.1.1", "1.1.1.1"))
194
195         #
196         # Send a general query (to the all router's address)
197         # expect VPP to respond with a membership report
198         #
199         p_g = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
200                IP(src=self.pg0.remote_ip4, dst='224.0.0.1', tos=0xc0) /
201                IGMPv3(type="Membership Query", mrcode=100) /
202                IGMPv3mq(gaddr="0.0.0.0"))
203
204         self.send(self.pg0, p_g)
205
206         capture = self.pg0.get_capture(1, timeout=10)
207         self.verify_report(capture[0],
208                            [IgmpRecord(h1.sg, "Mode Is Include")])
209
210         #
211         # Group specific query
212         #
213         p_gs = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
214                 IP(src=self.pg0.remote_ip4, dst='239.1.1.1', tos=0xc0,
215                    options=[IPOption(copy_flag=1, optclass="control",
216                                      option="router_alert")]) /
217                 IGMPv3(type="Membership Query", mrcode=100) /
218                 IGMPv3mq(gaddr="239.1.1.1"))
219
220         self.send(self.pg0, p_gs)
221
222         capture = self.pg0.get_capture(1, timeout=10)
223         self.verify_report(capture[0],
224                            [IgmpRecord(h1.sg, "Mode Is Include")])
225
226         #
227         # A group and source specific query, with the source matching
228         # the source VPP has
229         #
230         p_gs1 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
231                  IP(src=self.pg0.remote_ip4, dst='239.1.1.1', tos=0xc0,
232                     options=[IPOption(copy_flag=1, optclass="control",
233                                       option="router_alert")]) /
234                  IGMPv3(type="Membership Query", mrcode=100) /
235                  IGMPv3mq(gaddr="239.1.1.1", srcaddrs=["1.1.1.1"]))
236
237         self.send(self.pg0, p_gs1)
238
239         capture = self.pg0.get_capture(1, timeout=10)
240         self.verify_report(capture[0],
241                            [IgmpRecord(h1.sg, "Mode Is Include")])
242
243         #
244         # A group and source specific query, with the source NOT matching
245         # the source VPP has. There should be no response.
246         #
247         p_gs2 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
248                  IP(src=self.pg0.remote_ip4, dst='239.1.1.1', tos=0xc0,
249                     options=[IPOption(copy_flag=1, optclass="control",
250                                       option="router_alert")]) /
251                  IGMPv3(type="Membership Query", mrcode=100) /
252                  IGMPv3mq(gaddr="239.1.1.1", srcaddrs=["1.1.1.2"]))
253
254         self.send_and_assert_no_replies(self.pg0, p_gs2, timeout=10)
255
256         #
257         # A group and source specific query, with the multiple sources
258         # one of which matches the source VPP has.
259         # The report should contain only the source VPP has.
260         #
261         p_gs3 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
262                  IP(src=self.pg0.remote_ip4, dst='239.1.1.1', tos=0xc0,
263                     options=[IPOption(copy_flag=1, optclass="control",
264                                       option="router_alert")]) /
265                  IGMPv3(type="Membership Query", mrcode=100) /
266                  IGMPv3mq(gaddr="239.1.1.1",
267                           srcaddrs=["1.1.1.1", "1.1.1.2", "1.1.1.3"]))
268
269         self.send(self.pg0, p_gs3)
270
271         capture = self.pg0.get_capture(1, timeout=10)
272         self.verify_report(capture[0],
273                            [IgmpRecord(h1.sg, "Mode Is Include")])
274
275         #
276         # Two source and group specific queries in quick succession, the
277         # first does not have VPPs source the second does. then vice-versa
278         #
279         self.send(self.pg0, [p_gs2, p_gs1])
280         capture = self.pg0.get_capture(1, timeout=10)
281         self.verify_report(capture[0],
282                            [IgmpRecord(h1.sg, "Mode Is Include")])
283
284         self.send(self.pg0, [p_gs1, p_gs2])
285         capture = self.pg0.get_capture(1, timeout=10)
286         self.verify_report(capture[0],
287                            [IgmpRecord(h1.sg, "Mode Is Include")])
288
289         #
290         # remove state, expect the report for the removal
291         #
292         self.remove_group(h1)
293
294         dump = self.vapi.igmp_dump()
295         self.assertFalse(dump)
296
297         #
298         # A group with multiple sources
299         #
300         h2 = self.add_group(self.pg0,
301                             IgmpSG("239.1.1.1",
302                                    ["1.1.1.1", "1.1.1.2", "1.1.1.3"]))
303
304         # search for the corresponding state created in VPP
305         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
306         self.assertEqual(len(dump), 3)
307         for s in h2.sg.saddrs:
308             self.assertTrue(find_igmp_state(dump, self.pg0,
309                                             "239.1.1.1", s))
310         #
311         # Send a general query (to the all router's address)
312         # expect VPP to respond with a membership report will all sources
313         #
314         self.send(self.pg0, p_g)
315
316         capture = self.pg0.get_capture(1, timeout=10)
317         self.verify_report(capture[0],
318                            [IgmpRecord(h2.sg, "Mode Is Include")])
319
320         #
321         # Group and source specific query; some present some not
322         #
323         p_gs = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
324                 IP(src=self.pg0.remote_ip4, dst='239.1.1.1', tos=0xc0,
325                    options=[IPOption(copy_flag=1, optclass="control",
326                                      option="router_alert")]) /
327                 IGMPv3(type="Membership Query", mrcode=100) /
328                 IGMPv3mq(gaddr="239.1.1.1",
329                          srcaddrs=["1.1.1.1", "1.1.1.2", "1.1.1.4"]))
330
331         self.send(self.pg0, p_gs)
332
333         capture = self.pg0.get_capture(1, timeout=10)
334         self.verify_report(capture[0],
335                            [IgmpRecord(
336                                IgmpSG('239.1.1.1', ["1.1.1.1", "1.1.1.2"]),
337                                "Mode Is Include")])
338
339         #
340         # add loads more groups
341         #
342         h3 = self.add_group(self.pg0,
343                             IgmpSG("239.1.1.2",
344                                    ["2.1.1.1", "2.1.1.2", "2.1.1.3"]))
345         h4 = self.add_group(self.pg0,
346                             IgmpSG("239.1.1.3",
347                                    ["3.1.1.1", "3.1.1.2", "3.1.1.3"]))
348         h5 = self.add_group(self.pg0,
349                             IgmpSG("239.1.1.4",
350                                    ["4.1.1.1", "4.1.1.2", "4.1.1.3"]))
351         h6 = self.add_group(self.pg0,
352                             IgmpSG("239.1.1.5",
353                                    ["5.1.1.1", "5.1.1.2", "5.1.1.3"]))
354         h7 = self.add_group(self.pg0,
355                             IgmpSG("239.1.1.6",
356                                    ["6.1.1.1", "6.1.1.2",
357                                     "6.1.1.3", "6.1.1.4",
358                                     "6.1.1.5", "6.1.1.6",
359                                     "6.1.1.7", "6.1.1.8",
360                                     "6.1.1.9", "6.1.1.10",
361                                     "6.1.1.11", "6.1.1.12",
362                                     "6.1.1.13", "6.1.1.14",
363                                     "6.1.1.15", "6.1.1.16"]))
364
365         #
366         # general query.
367         # the order the groups come in is not important, so what is
368         # checked for is what VPP is sending today.
369         #
370         self.send(self.pg0, p_g)
371
372         capture = self.pg0.get_capture(1, timeout=10)
373
374         self.verify_report(capture[0],
375                            [IgmpRecord(h3.sg, "Mode Is Include"),
376                             IgmpRecord(h2.sg, "Mode Is Include"),
377                             IgmpRecord(h6.sg, "Mode Is Include"),
378                             IgmpRecord(h4.sg, "Mode Is Include"),
379                             IgmpRecord(h5.sg, "Mode Is Include"),
380                             IgmpRecord(h7.sg, "Mode Is Include")])
381
382         #
383         # modify a group to add and remove some sources
384         #
385         h7.sg = IgmpSG("239.1.1.6",
386                        ["6.1.1.1", "6.1.1.2",
387                         "6.1.1.5", "6.1.1.6",
388                         "6.1.1.7", "6.1.1.8",
389                         "6.1.1.9", "6.1.1.10",
390                         "6.1.1.11", "6.1.1.12",
391                         "6.1.1.13", "6.1.1.14",
392                         "6.1.1.15", "6.1.1.16",
393                         "6.1.1.17", "6.1.1.18"])
394
395         self.pg_enable_capture(self.pg_interfaces)
396         self.pg_start()
397         h7.add_vpp_config()
398
399         capture = self.pg0.get_capture(1, timeout=10)
400         self.verify_report(capture[0],
401                            [IgmpRecord(IgmpSG("239.1.1.6",
402                                               ["6.1.1.17", "6.1.1.18"]),
403                                        "Allow New Sources"),
404                             IgmpRecord(IgmpSG("239.1.1.6",
405                                               ["6.1.1.3", "6.1.1.4"]),
406                                        "Block Old Sources")])
407
408         #
409         # add an additional groups with many sources so that each group
410         # consumes the link MTU. We should therefore see multiple state
411         # state reports when queried.
412         #
413         self.vapi.sw_interface_set_mtu(self.pg0.sw_if_index, [560, 0, 0, 0])
414
415         src_list = []
416         for i in range(128):
417             src_list.append("10.1.1.%d" % i)
418
419         h8 = self.add_group(self.pg0,
420                             IgmpSG("238.1.1.1", src_list))
421         h9 = self.add_group(self.pg0,
422                             IgmpSG("238.1.1.2", src_list))
423
424         self.send(self.pg0, p_g)
425
426         capture = self.pg0.get_capture(4, timeout=10)
427
428         self.verify_report(capture[0],
429                            [IgmpRecord(h3.sg, "Mode Is Include"),
430                             IgmpRecord(h2.sg, "Mode Is Include"),
431                             IgmpRecord(h6.sg, "Mode Is Include"),
432                             IgmpRecord(h4.sg, "Mode Is Include"),
433                             IgmpRecord(h5.sg, "Mode Is Include")])
434         self.verify_report(capture[1],
435                            [IgmpRecord(h8.sg, "Mode Is Include")])
436         self.verify_report(capture[2],
437                            [IgmpRecord(h7.sg, "Mode Is Include")])
438         self.verify_report(capture[3],
439                            [IgmpRecord(h9.sg, "Mode Is Include")])
440
441         #
442         # drop the MTU further (so a 128 sized group won't fit)
443         #
444         self.vapi.sw_interface_set_mtu(self.pg0.sw_if_index, [512, 0, 0, 0])
445
446         self.pg_enable_capture(self.pg_interfaces)
447         self.pg_start()
448
449         h10 = VppHostState(self,
450                            IGMP_FILTER.INCLUDE,
451                            self.pg0.sw_if_index,
452                            IgmpSG("238.1.1.3", src_list))
453         h10.add_vpp_config()
454
455         capture = self.pg0.get_capture(2, timeout=10)
456
457         #
458         # remove state, expect the report for the removal
459         # the dump should be empty
460         #
461         self.vapi.sw_interface_set_mtu(self.pg0.sw_if_index, [600, 0, 0, 0])
462         self.remove_group(h8)
463         self.remove_group(h9)
464         self.remove_group(h2)
465         self.remove_group(h3)
466         self.remove_group(h4)
467         self.remove_group(h5)
468         self.remove_group(h6)
469         self.remove_group(h7)
470         self.remove_group(h10)
471
472         self.logger.info(self.vapi.cli("sh igmp config"))
473         self.assertFalse(self.vapi.igmp_dump())
474
475         #
476         # TODO
477         #  ADD STATE ON MORE INTERFACES
478         #
479
480         self.vapi.igmp_enable_disable(self.pg0.sw_if_index,
481                                       0,
482                                       IGMP_MODE.HOST)
483
484     def test_igmp_router(self):
485         """ IGMP Router Functions """
486
487         #
488         # Drop reports when not enabled
489         #
490         p_j = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
491                IP(src=self.pg0.remote_ip4, dst="224.0.0.22", tos=0xc0, ttl=1,
492                   options=[IPOption(copy_flag=1, optclass="control",
493                                     option="router_alert")]) /
494                IGMPv3(type="Version 3 Membership Report") /
495                IGMPv3mr(numgrp=1) /
496                IGMPv3gr(rtype="Allow New Sources",
497                         maddr="239.1.1.1", srcaddrs=["10.1.1.1", "10.1.1.2"]))
498         p_l = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
499                IP(src=self.pg0.remote_ip4, dst="224.0.0.22", tos=0xc0,
500                   options=[IPOption(copy_flag=1, optclass="control",
501                                     option="router_alert")]) /
502                IGMPv3(type="Version 3 Membership Report") /
503                IGMPv3mr(numgrp=1) /
504                IGMPv3gr(rtype="Block Old Sources",
505                         maddr="239.1.1.1", srcaddrs=["10.1.1.1", "10.1.1.2"]))
506
507         self.send(self.pg0, p_j)
508         self.assertFalse(self.vapi.igmp_dump())
509
510         #
511         # drop the default timer values so these tests execute in a
512         # reasonable time frame
513         #
514         self.vapi.cli("test igmp timers query 1 src 3 leave 1")
515
516         #
517         # enable router functions on the interface
518         #
519         self.pg_enable_capture(self.pg_interfaces)
520         self.pg_start()
521         self.vapi.igmp_enable_disable(self.pg0.sw_if_index,
522                                       1,
523                                       IGMP_MODE.ROUTER)
524         self.vapi.want_igmp_events(1)
525
526         #
527         # wait for router to send general query
528         #
529         for ii in range(3):
530             capture = self.pg0.get_capture(1, timeout=2)
531             self.verify_general_query(capture[0])
532             self.pg_enable_capture(self.pg_interfaces)
533             self.pg_start()
534
535         #
536         # re-send the report. VPP should now hold state for the new group
537         # VPP sends a notification that a new group has been joined
538         #
539         self.send(self.pg0, p_j)
540
541         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
542                                             "239.1.1.1", "10.1.1.1", 1))
543         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
544                                             "239.1.1.1", "10.1.1.2", 1))
545         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
546         self.assertEqual(len(dump), 2)
547         self.assertTrue(find_igmp_state(dump, self.pg0,
548                                         "239.1.1.1", "10.1.1.1"))
549         self.assertTrue(find_igmp_state(dump, self.pg0,
550                                         "239.1.1.1", "10.1.1.2"))
551
552         #
553         # wait for the per-source timer to expire
554         # the state should be reaped
555         # VPP sends a notification that the group has been left
556         #
557         self.assertTrue(wait_for_igmp_event(self, 4, self.pg0,
558                                             "239.1.1.1", "10.1.1.1", 0))
559         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
560                                             "239.1.1.1", "10.1.1.2", 0))
561         self.assertFalse(self.vapi.igmp_dump())
562
563         #
564         # resend the join. wait for two queries and then send a current-state
565         # record to include all sources. this should reset the expiry time
566         # on the sources and thus they will still be present in 2 seconds time.
567         # If the source timer was not refreshed, then the state would have
568         # expired in 3 seconds.
569         #
570         self.send(self.pg0, p_j)
571         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
572                                             "239.1.1.1", "10.1.1.1", 1))
573         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
574                                             "239.1.1.1", "10.1.1.2", 1))
575         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
576         self.assertEqual(len(dump), 2)
577
578         capture = self.pg0.get_capture(2, timeout=3)
579         self.verify_general_query(capture[0])
580         self.verify_general_query(capture[1])
581
582         p_cs = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
583                 IP(src=self.pg0.remote_ip4, dst="224.0.0.22", tos=0xc0,
584                    options=[IPOption(copy_flag=1, optclass="control",
585                                      option="router_alert")]) /
586                 IGMPv3(type="Version 3 Membership Report") /
587                 IGMPv3mr(numgrp=1) /
588                 IGMPv3gr(rtype="Mode Is Include",
589                          maddr="239.1.1.1", srcaddrs=["10.1.1.1", "10.1.1.2"]))
590
591         self.send(self.pg0, p_cs)
592
593         self.sleep(2)
594         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
595         self.assertEqual(len(dump), 2)
596         self.assertTrue(find_igmp_state(dump, self.pg0,
597                                         "239.1.1.1", "10.1.1.1"))
598         self.assertTrue(find_igmp_state(dump, self.pg0,
599                                         "239.1.1.1", "10.1.1.2"))
600
601         #
602         # wait for the per-source timer to expire
603         # the state should be reaped
604         #
605         self.assertTrue(wait_for_igmp_event(self, 4, self.pg0,
606                                             "239.1.1.1", "10.1.1.1", 0))
607         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
608                                             "239.1.1.1", "10.1.1.2", 0))
609         self.assertFalse(self.vapi.igmp_dump())
610
611         #
612         # resend the join, then a leave. Router sends a group+source
613         # specific query containing both sources
614         #
615         self.send(self.pg0, p_j)
616
617         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
618                                             "239.1.1.1", "10.1.1.1", 1))
619         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
620                                             "239.1.1.1", "10.1.1.2", 1))
621         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
622         self.assertEqual(len(dump), 2)
623
624         self.send(self.pg0, p_l)
625         capture = self.pg0.get_capture(1, timeout=3)
626         self.verify_group_query(capture[0], "239.1.1.1",
627                                 ["10.1.1.1", "10.1.1.2"])
628
629         #
630         # the group specific query drops the timeout to leave (=1) seconds
631         #
632         self.assertTrue(wait_for_igmp_event(self, 2, self.pg0,
633                                             "239.1.1.1", "10.1.1.1", 0))
634         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
635                                             "239.1.1.1", "10.1.1.2", 0))
636         self.assertFalse(self.vapi.igmp_dump())
637         self.assertFalse(self.vapi.igmp_dump())
638
639         #
640         # a TO_EX({}) / IN_EX({}) is treated like a (*,G) join
641         #
642         p_j = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
643                IP(src=self.pg0.remote_ip4, dst="224.0.0.22", tos=0xc0, ttl=1,
644                   options=[IPOption(copy_flag=1, optclass="control",
645                                     option="router_alert")]) /
646                IGMPv3(type="Version 3 Membership Report") /
647                IGMPv3mr(numgrp=1) /
648                IGMPv3gr(rtype="Change To Exclude Mode", maddr="239.1.1.2"))
649
650         self.send(self.pg0, p_j)
651
652         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
653                                             "239.1.1.2", "0.0.0.0", 1))
654
655         p_j = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
656                IP(src=self.pg0.remote_ip4, dst="224.0.0.22", tos=0xc0, ttl=1,
657                   options=[IPOption(copy_flag=1, optclass="control",
658                                     option="router_alert")]) /
659                IGMPv3(type="Version 3 Membership Report") /
660                IGMPv3mr(numgrp=1) /
661                IGMPv3gr(rtype="Mode Is Exclude", maddr="239.1.1.3"))
662
663         self.send(self.pg0, p_j)
664
665         self.assertTrue(wait_for_igmp_event(self, 1, self.pg0,
666                                             "239.1.1.3", "0.0.0.0", 1))
667
668         #
669         # A 'allow sources' for {} should be ignored as it should
670         # never be sent.
671         #
672         p_j = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
673                IP(src=self.pg0.remote_ip4, dst="224.0.0.22", tos=0xc0, ttl=1,
674                   options=[IPOption(copy_flag=1, optclass="control",
675                                     option="router_alert")]) /
676                IGMPv3(type="Version 3 Membership Report") /
677                IGMPv3mr(numgrp=1) /
678                IGMPv3gr(rtype="Allow New Sources", maddr="239.1.1.4"))
679
680         self.send(self.pg0, p_j)
681
682         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
683         self.assertTrue(find_igmp_state(dump, self.pg0,
684                                         "239.1.1.2", "0.0.0.0"))
685         self.assertTrue(find_igmp_state(dump, self.pg0,
686                                         "239.1.1.3", "0.0.0.0"))
687         self.assertFalse(find_igmp_state(dump, self.pg0,
688                                          "239.1.1.4", "0.0.0.0"))
689
690         #
691         # a TO_IN({}) and IS_IN({}) are treated like a (*,G) leave
692         #
693         self.vapi.cli("set logging class igmp level debug")
694         p_l = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
695                IP(src=self.pg0.remote_ip4, dst="224.0.0.22", tos=0xc0, ttl=1,
696                   options=[IPOption(copy_flag=1, optclass="control",
697                                     option="router_alert")]) /
698                IGMPv3(type="Version 3 Membership Report") /
699                IGMPv3mr(numgrp=1) /
700                IGMPv3gr(rtype="Change To Include Mode", maddr="239.1.1.2"))
701
702         self.send(self.pg0, p_l)
703         self.assertTrue(wait_for_igmp_event(self, 2, self.pg0,
704                                             "239.1.1.2", "0.0.0.0", 0))
705
706         p_l = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
707                IP(src=self.pg0.remote_ip4, dst="224.0.0.22", tos=0xc0, ttl=1,
708                   options=[IPOption(copy_flag=1, optclass="control",
709                                     option="router_alert")]) /
710                IGMPv3(type="Version 3 Membership Report") /
711                IGMPv3mr(numgrp=1) /
712                IGMPv3gr(rtype="Mode Is Include", maddr="239.1.1.3"))
713
714         self.send(self.pg0, p_l)
715
716         self.assertTrue(wait_for_igmp_event(self, 2, self.pg0,
717                                             "239.1.1.3", "0.0.0.0", 0))
718         self.assertFalse(self.vapi.igmp_dump(self.pg0.sw_if_index))
719
720         #
721         # disable router config
722         #
723         self.vapi.igmp_enable_disable(self.pg0.sw_if_index,
724                                       0,
725                                       IGMP_MODE.ROUTER)
726
727     def _create_igmpv3_pck(self, itf, rtype, maddr, srcaddrs):
728         p = (Ether(dst=itf.local_mac, src=itf.remote_mac) /
729              IP(src=itf.remote_ip4, dst="224.0.0.22", tos=0xc0, ttl=1,
730                 options=[IPOption(copy_flag=1, optclass="control",
731                                   option="router_alert")]) /
732              IGMPv3(type="Version 3 Membership Report") /
733              IGMPv3mr(numgrp=1) /
734              IGMPv3gr(rtype=rtype,
735                       maddr=maddr, srcaddrs=srcaddrs))
736         return p
737
738     def test_igmp_proxy_device(self):
739         """ IGMP proxy device """
740         self.pg2.admin_down()
741         self.pg2.unconfig_ip4()
742         self.pg2.set_table_ip4(0)
743         self.pg2.config_ip4()
744         self.pg2.admin_up()
745
746         self.vapi.cli('test igmp timers query 10 src 3 leave 1')
747
748         # enable IGMP
749         self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 1, IGMP_MODE.HOST)
750         self.vapi.igmp_enable_disable(self.pg1.sw_if_index, 1,
751                                       IGMP_MODE.ROUTER)
752         self.vapi.igmp_enable_disable(self.pg2.sw_if_index, 1,
753                                       IGMP_MODE.ROUTER)
754
755         # create IGMP proxy device
756         self.vapi.igmp_proxy_device_add_del(0, self.pg0.sw_if_index, 1)
757         self.vapi.igmp_proxy_device_add_del_interface(0,
758                                                       self.pg1.sw_if_index, 1)
759         self.vapi.igmp_proxy_device_add_del_interface(0,
760                                                       self.pg2.sw_if_index, 1)
761
762         # send join on pg1. join should be proxied by pg0
763         p_j = self._create_igmpv3_pck(self.pg1, "Allow New Sources",
764                                       "239.1.1.1", ["10.1.1.1", "10.1.1.2"])
765         self.send(self.pg1, p_j)
766
767         capture = self.pg0.get_capture(1, timeout=1)
768         self.verify_report(capture[0], [IgmpRecord(IgmpSG("239.1.1.1",
769                            ["10.1.1.1", "10.1.1.2"]), "Allow New Sources")])
770         self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
771
772         # send join on pg2. join should be proxied by pg0.
773         # the group should contain only 10.1.1.3 as
774         # 10.1.1.1 was already reported
775         p_j = self._create_igmpv3_pck(self.pg2, "Allow New Sources",
776                                       "239.1.1.1", ["10.1.1.1", "10.1.1.3"])
777         self.send(self.pg2, p_j)
778
779         capture = self.pg0.get_capture(1, timeout=1)
780         self.verify_report(capture[0], [IgmpRecord(IgmpSG("239.1.1.1",
781                            ["10.1.1.3"]), "Allow New Sources")])
782         self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
783
784         # send leave on pg2. leave for 10.1.1.3 should be proxyed
785         # as pg2 was the only interface interested in 10.1.1.3
786         p_l = self._create_igmpv3_pck(self.pg2, "Block Old Sources",
787                                       "239.1.1.1", ["10.1.1.3"])
788         self.send(self.pg2, p_l)
789
790         capture = self.pg0.get_capture(1, timeout=2)
791         self.verify_report(capture[0], [IgmpRecord(IgmpSG("239.1.1.1",
792                            ["10.1.1.3"]), "Block Old Sources")])
793         self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
794
795         # disable igmp on pg1 (also removes interface from proxy device)
796         # proxy leave for 10.1.1.2. pg2 is still interested in 10.1.1.1
797         self.pg_enable_capture(self.pg_interfaces)
798         self.vapi.igmp_enable_disable(self.pg1.sw_if_index, 0,
799                                       IGMP_MODE.ROUTER)
800
801         capture = self.pg0.get_capture(1, timeout=1)
802         self.verify_report(capture[0], [IgmpRecord(IgmpSG("239.1.1.1",
803                            ["10.1.1.2"]), "Block Old Sources")])
804         self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
805
806         # disable IGMP on pg0 and pg1.
807         #   disabling IGMP on pg0 (proxy device upstream interface)
808         #   removes this proxy device
809         self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 0, IGMP_MODE.HOST)
810         self.vapi.igmp_enable_disable(self.pg2.sw_if_index, 0,
811                                       IGMP_MODE.ROUTER)
812         self.assertFalse(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
813
814
815 if __name__ == '__main__':
816     unittest.main(testRunner=VppTestRunner)