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