ethernet: check destination mac for L3 in ethernet-input node
[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 VppTestCase
11 from asfframework import VppTestRunner, tag_fixme_vpp_workers
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(
222                         copy_flag=1, optclass="control", option="router_alert", length=4
223                     )
224                 ],
225             )
226             / IGMPv3(type="Membership Query", mrcode=100)
227             / IGMPv3mq(gaddr="239.1.1.1")
228         )
229
230         self.send(self.pg0, p_gs)
231
232         capture = self.pg0.get_capture(1, timeout=10)
233         self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
234
235         #
236         # A group and source specific query, with the source matching
237         # the source VPP has
238         #
239         p_gs1 = (
240             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
241             / IP(
242                 src=self.pg0.remote_ip4,
243                 dst="239.1.1.1",
244                 tos=0xC0,
245                 options=[
246                     IPOption(
247                         copy_flag=1, optclass="control", option="router_alert", length=4
248                     )
249                 ],
250             )
251             / IGMPv3(type="Membership Query", mrcode=100)
252             / IGMPv3mq(gaddr="239.1.1.1", srcaddrs=["1.1.1.1"])
253         )
254
255         self.send(self.pg0, p_gs1)
256
257         capture = self.pg0.get_capture(1, timeout=10)
258         self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
259
260         #
261         # A group and source specific query that reports more sources
262         # than the packet actually has.
263         #
264         p_gs2 = (
265             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
266             / IP(
267                 src=self.pg0.remote_ip4,
268                 dst="239.1.1.1",
269                 tos=0xC0,
270                 options=[
271                     IPOption(
272                         copy_flag=1, optclass="control", option="router_alert", length=4
273                     )
274                 ],
275             )
276             / IGMPv3(type="Membership Query", mrcode=100)
277             / IGMPv3mq(gaddr="239.1.1.1", numsrc=4, srcaddrs=["1.1.1.1"])
278         )
279
280         self.send_and_assert_no_replies(self.pg0, p_gs2, timeout=10)
281
282         #
283         # A group and source specific query, with the source NOT matching
284         # the source VPP has. There should be no response.
285         #
286         p_gs2 = (
287             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
288             / IP(
289                 src=self.pg0.remote_ip4,
290                 dst="239.1.1.1",
291                 tos=0xC0,
292                 options=[
293                     IPOption(
294                         copy_flag=1, optclass="control", option="router_alert", length=4
295                     )
296                 ],
297             )
298             / IGMPv3(type="Membership Query", mrcode=100)
299             / IGMPv3mq(gaddr="239.1.1.1", srcaddrs=["1.1.1.2"])
300         )
301
302         self.send_and_assert_no_replies(self.pg0, p_gs2, timeout=10)
303
304         #
305         # A group and source specific query, with the multiple sources
306         # one of which matches the source VPP has.
307         # The report should contain only the source VPP has.
308         #
309         p_gs3 = (
310             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
311             / IP(
312                 src=self.pg0.remote_ip4,
313                 dst="239.1.1.1",
314                 tos=0xC0,
315                 options=[
316                     IPOption(
317                         copy_flag=1, optclass="control", option="router_alert", length=4
318                     )
319                 ],
320             )
321             / IGMPv3(type="Membership Query", mrcode=100)
322             / IGMPv3mq(gaddr="239.1.1.1", srcaddrs=["1.1.1.1", "1.1.1.2", "1.1.1.3"])
323         )
324
325         self.send(self.pg0, p_gs3)
326
327         capture = self.pg0.get_capture(1, timeout=10)
328         self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
329
330         #
331         # Two source and group specific queries in quick succession, the
332         # first does not have VPPs source the second does. then vice-versa
333         #
334         self.send(self.pg0, [p_gs2, p_gs1])
335         capture = self.pg0.get_capture(1, timeout=10)
336         self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
337
338         self.send(self.pg0, [p_gs1, p_gs2])
339         capture = self.pg0.get_capture(1, timeout=10)
340         self.verify_report(capture[0], [IgmpRecord(h1.sg, "Mode Is Include")])
341
342         #
343         # remove state, expect the report for the removal
344         #
345         self.remove_group(h1)
346
347         dump = self.vapi.igmp_dump()
348         self.assertFalse(dump)
349
350         #
351         # A group with multiple sources
352         #
353         h2 = self.add_group(
354             self.pg0, IgmpSG("239.1.1.1", ["1.1.1.1", "1.1.1.2", "1.1.1.3"])
355         )
356
357         # search for the corresponding state created in VPP
358         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
359         self.assertEqual(len(dump), 3)
360         for s in h2.sg.saddrs:
361             self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", s))
362         #
363         # Send a general query (to the all router's address)
364         # expect VPP to respond with a membership report will all sources
365         #
366         self.send(self.pg0, p_g)
367
368         capture = self.pg0.get_capture(1, timeout=10)
369         self.verify_report(capture[0], [IgmpRecord(h2.sg, "Mode Is Include")])
370
371         #
372         # Group and source specific query; some present some not
373         #
374         p_gs = (
375             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
376             / IP(
377                 src=self.pg0.remote_ip4,
378                 dst="239.1.1.1",
379                 tos=0xC0,
380                 options=[
381                     IPOption(
382                         copy_flag=1, optclass="control", option="router_alert", length=4
383                     )
384                 ],
385             )
386             / IGMPv3(type="Membership Query", mrcode=100)
387             / IGMPv3mq(gaddr="239.1.1.1", srcaddrs=["1.1.1.1", "1.1.1.2", "1.1.1.4"])
388         )
389
390         self.send(self.pg0, p_gs)
391
392         capture = self.pg0.get_capture(1, timeout=10)
393         self.verify_report(
394             capture[0],
395             [
396                 IgmpRecord(
397                     IgmpSG("239.1.1.1", ["1.1.1.1", "1.1.1.2"]), "Mode Is Include"
398                 )
399             ],
400         )
401
402         #
403         # add loads more groups
404         #
405         h3 = self.add_group(
406             self.pg0, IgmpSG("239.1.1.2", ["2.1.1.1", "2.1.1.2", "2.1.1.3"])
407         )
408         h4 = self.add_group(
409             self.pg0, IgmpSG("239.1.1.3", ["3.1.1.1", "3.1.1.2", "3.1.1.3"])
410         )
411         h5 = self.add_group(
412             self.pg0, IgmpSG("239.1.1.4", ["4.1.1.1", "4.1.1.2", "4.1.1.3"])
413         )
414         h6 = self.add_group(
415             self.pg0, IgmpSG("239.1.1.5", ["5.1.1.1", "5.1.1.2", "5.1.1.3"])
416         )
417         h7 = self.add_group(
418             self.pg0,
419             IgmpSG(
420                 "239.1.1.6",
421                 [
422                     "6.1.1.1",
423                     "6.1.1.2",
424                     "6.1.1.3",
425                     "6.1.1.4",
426                     "6.1.1.5",
427                     "6.1.1.6",
428                     "6.1.1.7",
429                     "6.1.1.8",
430                     "6.1.1.9",
431                     "6.1.1.10",
432                     "6.1.1.11",
433                     "6.1.1.12",
434                     "6.1.1.13",
435                     "6.1.1.14",
436                     "6.1.1.15",
437                     "6.1.1.16",
438                 ],
439             ),
440         )
441
442         #
443         # general query.
444         # the order the groups come in is not important, so what is
445         # checked for is what VPP is sending today.
446         #
447         self.send(self.pg0, p_g)
448
449         capture = self.pg0.get_capture(1, timeout=10)
450
451         self.verify_report(
452             capture[0],
453             [
454                 IgmpRecord(h3.sg, "Mode Is Include"),
455                 IgmpRecord(h2.sg, "Mode Is Include"),
456                 IgmpRecord(h6.sg, "Mode Is Include"),
457                 IgmpRecord(h4.sg, "Mode Is Include"),
458                 IgmpRecord(h5.sg, "Mode Is Include"),
459                 IgmpRecord(h7.sg, "Mode Is Include"),
460             ],
461         )
462
463         #
464         # modify a group to add and remove some sources
465         #
466         h7.sg = IgmpSG(
467             "239.1.1.6",
468             [
469                 "6.1.1.1",
470                 "6.1.1.2",
471                 "6.1.1.5",
472                 "6.1.1.6",
473                 "6.1.1.7",
474                 "6.1.1.8",
475                 "6.1.1.9",
476                 "6.1.1.10",
477                 "6.1.1.11",
478                 "6.1.1.12",
479                 "6.1.1.13",
480                 "6.1.1.14",
481                 "6.1.1.15",
482                 "6.1.1.16",
483                 "6.1.1.17",
484                 "6.1.1.18",
485             ],
486         )
487
488         self.pg_enable_capture(self.pg_interfaces)
489         self.pg_start()
490         h7.add_vpp_config()
491
492         capture = self.pg0.get_capture(1, timeout=10)
493         self.verify_report(
494             capture[0],
495             [
496                 IgmpRecord(
497                     IgmpSG("239.1.1.6", ["6.1.1.17", "6.1.1.18"]), "Allow New Sources"
498                 ),
499                 IgmpRecord(
500                     IgmpSG("239.1.1.6", ["6.1.1.3", "6.1.1.4"]), "Block Old Sources"
501                 ),
502             ],
503         )
504
505         #
506         # add an additional groups with many sources so that each group
507         # consumes the link MTU. We should therefore see multiple state
508         # state reports when queried.
509         #
510         self.vapi.sw_interface_set_mtu(self.pg0.sw_if_index, [560, 0, 0, 0])
511
512         src_list = []
513         for i in range(128):
514             src_list.append("10.1.1.%d" % i)
515
516         h8 = self.add_group(self.pg0, IgmpSG("238.1.1.1", src_list))
517         h9 = self.add_group(self.pg0, IgmpSG("238.1.1.2", src_list))
518
519         self.send(self.pg0, p_g)
520
521         capture = self.pg0.get_capture(4, timeout=10)
522
523         self.verify_report(
524             capture[0],
525             [
526                 IgmpRecord(h3.sg, "Mode Is Include"),
527                 IgmpRecord(h2.sg, "Mode Is Include"),
528                 IgmpRecord(h6.sg, "Mode Is Include"),
529                 IgmpRecord(h4.sg, "Mode Is Include"),
530                 IgmpRecord(h5.sg, "Mode Is Include"),
531             ],
532         )
533         self.verify_report(capture[1], [IgmpRecord(h8.sg, "Mode Is Include")])
534         self.verify_report(capture[2], [IgmpRecord(h7.sg, "Mode Is Include")])
535         self.verify_report(capture[3], [IgmpRecord(h9.sg, "Mode Is Include")])
536
537         #
538         # drop the MTU further (so a 128 sized group won't fit)
539         #
540         self.vapi.sw_interface_set_mtu(self.pg0.sw_if_index, [512, 0, 0, 0])
541
542         self.pg_enable_capture(self.pg_interfaces)
543         self.pg_start()
544
545         h10 = VppHostState(
546             self,
547             IGMP_FILTER.INCLUDE,
548             self.pg0.sw_if_index,
549             IgmpSG("238.1.1.3", src_list),
550         )
551         h10.add_vpp_config()
552
553         capture = self.pg0.get_capture(2, timeout=10)
554         # wait for a little bit
555         self.sleep(1)
556
557         #
558         # remove state, expect the report for the removal
559         # the dump should be empty
560         #
561         self.vapi.sw_interface_set_mtu(self.pg0.sw_if_index, [600, 0, 0, 0])
562         self.remove_group(h8)
563         self.remove_group(h9)
564         self.remove_group(h2)
565         self.remove_group(h3)
566         self.remove_group(h4)
567         self.remove_group(h5)
568         self.remove_group(h6)
569         self.remove_group(h7)
570         self.remove_group(h10)
571
572         self.logger.info(self.vapi.cli("sh igmp config"))
573         self.assertFalse(self.vapi.igmp_dump())
574
575         #
576         # TODO
577         #  ADD STATE ON MORE INTERFACES
578         #
579
580         self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 0, IGMP_MODE.HOST)
581
582     def test_igmp_router(self):
583         """IGMP Router Functions"""
584
585         #
586         # Drop reports when not enabled
587         #
588         p_j = (
589             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
590             / IP(
591                 src=self.pg0.remote_ip4,
592                 dst="224.0.0.22",
593                 tos=0xC0,
594                 ttl=1,
595                 options=[
596                     IPOption(
597                         copy_flag=1, optclass="control", option="router_alert", length=4
598                     )
599                 ],
600             )
601             / IGMPv3(type="Version 3 Membership Report")
602             / IGMPv3mr(numgrp=1)
603             / IGMPv3gr(
604                 rtype="Allow New Sources",
605                 maddr="239.1.1.1",
606                 srcaddrs=["10.1.1.1", "10.1.1.2"],
607             )
608         )
609         p_l = (
610             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
611             / IP(
612                 src=self.pg0.remote_ip4,
613                 dst="224.0.0.22",
614                 tos=0xC0,
615                 options=[
616                     IPOption(
617                         copy_flag=1, optclass="control", option="router_alert", length=4
618                     )
619                 ],
620             )
621             / IGMPv3(type="Version 3 Membership Report")
622             / IGMPv3mr(numgrp=1)
623             / IGMPv3gr(
624                 rtype="Block Old Sources",
625                 maddr="239.1.1.1",
626                 srcaddrs=["10.1.1.1", "10.1.1.2"],
627             )
628         )
629
630         self.send(self.pg0, p_j)
631         self.assertFalse(self.vapi.igmp_dump())
632
633         #
634         # drop the default timer values so these tests execute in a
635         # reasonable time frame
636         #
637         self.vapi.cli("test igmp timers query 1 src 3 leave 1")
638
639         #
640         # enable router functions on the interface
641         #
642         self.pg_enable_capture(self.pg_interfaces)
643         self.pg_start()
644         self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 1, IGMP_MODE.ROUTER)
645         self.vapi.want_igmp_events(1)
646
647         #
648         # wait for router to send general query
649         #
650         for ii in range(3):
651             capture = self.pg0.get_capture(1, timeout=2)
652             self.verify_general_query(capture[0])
653             self.pg_enable_capture(self.pg_interfaces)
654             self.pg_start()
655
656         #
657         # re-send the report. VPP should now hold state for the new group
658         # VPP sends a notification that a new group has been joined
659         #
660         self.send(self.pg0, p_j)
661
662         self.assertTrue(
663             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.1", 1)
664         )
665         self.assertTrue(
666             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 1)
667         )
668         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
669         self.assertEqual(len(dump), 2)
670         self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", "10.1.1.1"))
671         self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", "10.1.1.2"))
672
673         #
674         # wait for the per-source timer to expire
675         # the state should be reaped
676         # VPP sends a notification that the group has been left
677         #
678         self.assertTrue(
679             wait_for_igmp_event(self, 4, self.pg0, "239.1.1.1", "10.1.1.1", 0)
680         )
681         self.assertTrue(
682             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 0)
683         )
684         self.assertFalse(self.vapi.igmp_dump())
685
686         #
687         # resend the join. wait for two queries and then send a current-state
688         # record to include all sources. this should reset the expiry time
689         # on the sources and thus they will still be present in 2 seconds time.
690         # If the source timer was not refreshed, then the state would have
691         # expired in 3 seconds.
692         #
693         self.send(self.pg0, p_j)
694         self.assertTrue(
695             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.1", 1)
696         )
697         self.assertTrue(
698             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 1)
699         )
700         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
701         self.assertEqual(len(dump), 2)
702
703         capture = self.pg0.get_capture(2, timeout=3)
704         self.verify_general_query(capture[0])
705         self.verify_general_query(capture[1])
706
707         p_cs = (
708             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
709             / IP(
710                 src=self.pg0.remote_ip4,
711                 dst="224.0.0.22",
712                 tos=0xC0,
713                 options=[
714                     IPOption(
715                         copy_flag=1, optclass="control", option="router_alert", length=4
716                     )
717                 ],
718             )
719             / IGMPv3(type="Version 3 Membership Report")
720             / IGMPv3mr(numgrp=1)
721             / IGMPv3gr(
722                 rtype="Mode Is Include",
723                 maddr="239.1.1.1",
724                 srcaddrs=["10.1.1.1", "10.1.1.2"],
725             )
726         )
727
728         self.send(self.pg0, p_cs)
729
730         self.sleep(2)
731         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
732         self.assertEqual(len(dump), 2)
733         self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", "10.1.1.1"))
734         self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.1", "10.1.1.2"))
735
736         #
737         # wait for the per-source timer to expire
738         # the state should be reaped
739         #
740         self.assertTrue(
741             wait_for_igmp_event(self, 4, self.pg0, "239.1.1.1", "10.1.1.1", 0)
742         )
743         self.assertTrue(
744             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 0)
745         )
746         self.assertFalse(self.vapi.igmp_dump())
747
748         #
749         # resend the join, then a leave. Router sends a group+source
750         # specific query containing both sources
751         #
752         self.send(self.pg0, p_j)
753
754         self.assertTrue(
755             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.1", 1)
756         )
757         self.assertTrue(
758             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 1)
759         )
760         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
761         self.assertEqual(len(dump), 2)
762
763         self.send(self.pg0, p_l)
764         capture = self.pg0.get_capture(1, timeout=3)
765         self.verify_group_query(capture[0], "239.1.1.1", ["10.1.1.1", "10.1.1.2"])
766
767         #
768         # the group specific query drops the timeout to leave (=1) seconds
769         #
770         self.assertTrue(
771             wait_for_igmp_event(self, 2, self.pg0, "239.1.1.1", "10.1.1.1", 0)
772         )
773         self.assertTrue(
774             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.1", "10.1.1.2", 0)
775         )
776         self.assertFalse(self.vapi.igmp_dump())
777         self.assertFalse(self.vapi.igmp_dump())
778
779         #
780         # a TO_EX({}) / IN_EX({}) is treated like a (*,G) join
781         #
782         p_j = (
783             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
784             / IP(
785                 src=self.pg0.remote_ip4,
786                 dst="224.0.0.22",
787                 tos=0xC0,
788                 ttl=1,
789                 options=[
790                     IPOption(
791                         copy_flag=1, optclass="control", option="router_alert", length=4
792                     )
793                 ],
794             )
795             / IGMPv3(type="Version 3 Membership Report")
796             / IGMPv3mr(numgrp=1)
797             / IGMPv3gr(rtype="Change To Exclude Mode", maddr="239.1.1.2")
798         )
799
800         self.send(self.pg0, p_j)
801
802         self.assertTrue(
803             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.2", "0.0.0.0", 1)
804         )
805
806         p_j = (
807             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
808             / IP(
809                 src=self.pg0.remote_ip4,
810                 dst="224.0.0.22",
811                 tos=0xC0,
812                 ttl=1,
813                 options=[
814                     IPOption(
815                         copy_flag=1, optclass="control", option="router_alert", length=4
816                     )
817                 ],
818             )
819             / IGMPv3(type="Version 3 Membership Report")
820             / IGMPv3mr(numgrp=1)
821             / IGMPv3gr(rtype="Mode Is Exclude", maddr="239.1.1.3")
822         )
823
824         self.send(self.pg0, p_j)
825
826         self.assertTrue(
827             wait_for_igmp_event(self, 1, self.pg0, "239.1.1.3", "0.0.0.0", 1)
828         )
829
830         #
831         # A 'allow sources' for {} should be ignored as it should
832         # never be sent.
833         #
834         p_j = (
835             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
836             / IP(
837                 src=self.pg0.remote_ip4,
838                 dst="224.0.0.22",
839                 tos=0xC0,
840                 ttl=1,
841                 options=[
842                     IPOption(
843                         copy_flag=1, optclass="control", option="router_alert", length=4
844                     )
845                 ],
846             )
847             / IGMPv3(type="Version 3 Membership Report")
848             / IGMPv3mr(numgrp=1)
849             / IGMPv3gr(rtype="Allow New Sources", maddr="239.1.1.4")
850         )
851
852         self.send(self.pg0, p_j)
853
854         dump = self.vapi.igmp_dump(self.pg0.sw_if_index)
855         self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.2", "0.0.0.0"))
856         self.assertTrue(find_igmp_state(dump, self.pg0, "239.1.1.3", "0.0.0.0"))
857         self.assertFalse(find_igmp_state(dump, self.pg0, "239.1.1.4", "0.0.0.0"))
858
859         #
860         # a TO_IN({}) and IS_IN({}) are treated like a (*,G) leave
861         #
862         self.vapi.cli("set logging class igmp level debug")
863         p_l = (
864             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
865             / IP(
866                 src=self.pg0.remote_ip4,
867                 dst="224.0.0.22",
868                 tos=0xC0,
869                 ttl=1,
870                 options=[
871                     IPOption(
872                         copy_flag=1, optclass="control", option="router_alert", length=4
873                     )
874                 ],
875             )
876             / IGMPv3(type="Version 3 Membership Report")
877             / IGMPv3mr(numgrp=1)
878             / IGMPv3gr(rtype="Change To Include Mode", maddr="239.1.1.2")
879         )
880
881         self.send(self.pg0, p_l)
882         self.assertTrue(
883             wait_for_igmp_event(self, 2, self.pg0, "239.1.1.2", "0.0.0.0", 0)
884         )
885
886         p_l = (
887             Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac)
888             / IP(
889                 src=self.pg0.remote_ip4,
890                 dst="224.0.0.22",
891                 tos=0xC0,
892                 ttl=1,
893                 options=[
894                     IPOption(
895                         copy_flag=1, optclass="control", option="router_alert", length=4
896                     )
897                 ],
898             )
899             / IGMPv3(type="Version 3 Membership Report")
900             / IGMPv3mr(numgrp=1)
901             / IGMPv3gr(rtype="Mode Is Include", maddr="239.1.1.3")
902         )
903
904         self.send(self.pg0, p_l)
905
906         self.assertTrue(
907             wait_for_igmp_event(self, 2, self.pg0, "239.1.1.3", "0.0.0.0", 0)
908         )
909         self.assertFalse(self.vapi.igmp_dump(self.pg0.sw_if_index))
910
911         #
912         # disable router config
913         #
914         self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 0, IGMP_MODE.ROUTER)
915
916     def _create_igmpv3_pck(self, itf, rtype, maddr, srcaddrs):
917         p = (
918             Ether(dst=itf.local_mac, src=itf.remote_mac)
919             / IP(
920                 src=itf.remote_ip4,
921                 dst="224.0.0.22",
922                 tos=0xC0,
923                 ttl=1,
924                 options=[
925                     IPOption(
926                         copy_flag=1, optclass="control", option="router_alert", length=4
927                     )
928                 ],
929             )
930             / IGMPv3(type="Version 3 Membership Report")
931             / IGMPv3mr(numgrp=1)
932             / IGMPv3gr(rtype=rtype, maddr=maddr, srcaddrs=srcaddrs)
933         )
934         return p
935
936     def test_igmp_proxy_device(self):
937         """IGMP proxy device"""
938         self.pg2.admin_down()
939         self.pg2.unconfig_ip4()
940         self.pg2.set_table_ip4(0)
941         self.pg2.config_ip4()
942         self.pg2.admin_up()
943
944         self.vapi.cli("test igmp timers query 10 src 3 leave 1")
945
946         # enable IGMP
947         self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 1, IGMP_MODE.HOST)
948         self.vapi.igmp_enable_disable(self.pg1.sw_if_index, 1, IGMP_MODE.ROUTER)
949         self.vapi.igmp_enable_disable(self.pg2.sw_if_index, 1, IGMP_MODE.ROUTER)
950
951         # create IGMP proxy device
952         self.vapi.igmp_proxy_device_add_del(0, self.pg0.sw_if_index, 1)
953         self.vapi.igmp_proxy_device_add_del_interface(0, self.pg1.sw_if_index, 1)
954         self.vapi.igmp_proxy_device_add_del_interface(0, self.pg2.sw_if_index, 1)
955
956         # send join on pg1. join should be proxied by pg0
957         p_j = self._create_igmpv3_pck(
958             self.pg1, "Allow New Sources", "239.1.1.1", ["10.1.1.1", "10.1.1.2"]
959         )
960         self.send(self.pg1, p_j)
961
962         capture = self.pg0.get_capture(1, timeout=1)
963         self.verify_report(
964             capture[0],
965             [
966                 IgmpRecord(
967                     IgmpSG("239.1.1.1", ["10.1.1.1", "10.1.1.2"]), "Allow New Sources"
968                 )
969             ],
970         )
971         self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
972
973         # send join on pg2. join should be proxied by pg0.
974         # the group should contain only 10.1.1.3 as
975         # 10.1.1.1 was already reported
976         p_j = self._create_igmpv3_pck(
977             self.pg2, "Allow New Sources", "239.1.1.1", ["10.1.1.1", "10.1.1.3"]
978         )
979         self.send(self.pg2, p_j)
980
981         capture = self.pg0.get_capture(1, timeout=1)
982         self.verify_report(
983             capture[0],
984             [IgmpRecord(IgmpSG("239.1.1.1", ["10.1.1.3"]), "Allow New Sources")],
985         )
986         self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
987
988         # send leave on pg2. leave for 10.1.1.3 should be proxyed
989         # as pg2 was the only interface interested in 10.1.1.3
990         p_l = self._create_igmpv3_pck(
991             self.pg2, "Block Old Sources", "239.1.1.1", ["10.1.1.3"]
992         )
993         self.send(self.pg2, p_l)
994
995         capture = self.pg0.get_capture(1, timeout=2)
996         self.verify_report(
997             capture[0],
998             [IgmpRecord(IgmpSG("239.1.1.1", ["10.1.1.3"]), "Block Old Sources")],
999         )
1000         self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
1001
1002         # disable igmp on pg1 (also removes interface from proxy device)
1003         # proxy leave for 10.1.1.2. pg2 is still interested in 10.1.1.1
1004         self.pg_enable_capture(self.pg_interfaces)
1005         self.vapi.igmp_enable_disable(self.pg1.sw_if_index, 0, IGMP_MODE.ROUTER)
1006
1007         capture = self.pg0.get_capture(1, timeout=1)
1008         self.verify_report(
1009             capture[0],
1010             [IgmpRecord(IgmpSG("239.1.1.1", ["10.1.1.2"]), "Block Old Sources")],
1011         )
1012         self.assertTrue(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
1013
1014         # disable IGMP on pg0 and pg1.
1015         #   disabling IGMP on pg0 (proxy device upstream interface)
1016         #   removes this proxy device
1017         self.vapi.igmp_enable_disable(self.pg0.sw_if_index, 0, IGMP_MODE.HOST)
1018         self.vapi.igmp_enable_disable(self.pg2.sw_if_index, 0, IGMP_MODE.ROUTER)
1019         self.assertFalse(find_mroute(self, "239.1.1.1", "0.0.0.0", 32))
1020
1021
1022 if __name__ == "__main__":
1023     unittest.main(testRunner=VppTestRunner)