1#!/usr/bin/env python3
2#
3#  Copyright (c) 2021, The OpenThread Authors.
4#  All rights reserved.
5#
6#  Redistribution and use in source and binary forms, with or without
7#  modification, are permitted provided that the following conditions are met:
8#  1. Redistributions of source code must retain the above copyright
9#     notice, this list of conditions and the following disclaimer.
10#  2. Redistributions in binary form must reproduce the above copyright
11#     notice, this list of conditions and the following disclaimer in the
12#     documentation and/or other materials provided with the distribution.
13#  3. Neither the name of the copyright holder nor the
14#     names of its contributors may be used to endorse or promote products
15#     derived from this software without specific prior written permission.
16#
17#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
18#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27#  POSSIBILITY OF SUCH DAMAGE.
28#
29
30import logging
31import unittest
32
33import pktverify
34from pktverify import packet_verifier, packet_filter
35from pktverify.consts import MA1, MA1g, MA2, PBBR_ALOC
36import config
37import thread_cert
38
39# Test description:
40# The purpose of this test case is to verify that a Thread device is able to
41# register a multicast address with a Primary BBR and that the Primary BBR can
42# notify devices on the backbone link of the multicast address. The test also
43# verifies that an IPv6 multicast packet originating from the backbone is
44# correctly forwarded (in the Thread Network) to the device that registered the
45# multicast address.
46#
47# Topology:
48#    ----------------(eth)------------------
49#           |                  |     |
50#          BR1 (Leader) ----- BR2   HOST
51#           |                  |
52#           |                  |
53#          TD --------(when TD is router)
54#
55
56BR_1 = 1
57BR_2 = 2
58TD = 3
59HOST = 4
60
61CHANNEL1 = 18
62
63MLR_NOTIFICATION_TIMEOUT = 3600
64
65
66class MATN_02_MLRFirstUse(thread_cert.TestCase):
67    USE_MESSAGE_FACTORY = False
68
69    TOPOLOGY = {
70        BR_1: {
71            'name': 'BR_1',
72            'is_otbr': True,
73            'allowlist': [BR_2, TD],
74            'version': '1.2',
75        },
76        BR_2: {
77            'name': 'BR_2',
78            'allowlist': [BR_1, TD],
79            'is_otbr': True,
80            'version': '1.2',
81        },
82        TD: {
83            'name': 'TD',
84            'allowlist': [BR_1, BR_2],
85            'version': '1.2',
86        },
87        HOST: {
88            'name': 'Host',
89            'is_host': True
90        },
91    }
92
93    def test(self):
94        br1 = self.nodes[BR_1]
95        br2 = self.nodes[BR_2]
96        td = self.nodes[TD]
97        host = self.nodes[HOST]
98
99        br1.start()
100        self.simulator.go(config.LEADER_STARTUP_DELAY)
101        self.assertEqual('leader', br1.get_state())
102        self.assertTrue(br1.is_primary_backbone_router)
103
104        td.start()
105        self.simulator.go(5)
106        self.assertEqual('router', td.get_state())
107
108        br2.start()
109        self.simulator.go(5)
110        self.assertEqual('router', br2.get_state())
111
112        host.start(start_radvd=False)
113        self.simulator.go(10)
114
115        # 1. TD registers for multicast address, MA1, at BR_1.
116        td.add_ipmaddr(MA1)
117        self.simulator.go(5)
118
119        # 7. Host sends a ping packet to the multicast address, MA1. TD should
120        # respond to the ping request.
121        self.assertTrue(
122            host.ping(MA1, backbone=True, ttl=10, interface=host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0]))
123        self.simulator.go(5)
124
125        # 11. Host sends a ping packet to the multicast address, MA2. No one
126        # should respond.
127        self.assertFalse(
128            host.ping(MA2, backbone=True, ttl=10, interface=host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0]))
129        self.simulator.go(5)
130
131        # 14. Host sends a ping packet to the multicast address, MA1g. No one
132        # should respond.
133        self.assertFalse(
134            host.ping(MA1g, backbone=True, ttl=10, interface=host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0]))
135        self.simulator.go(5)
136
137        # 17. Host sends a ping packet to the global unicast address of BR2. BR2
138        # should respond.
139        self.assertTrue(host.ping(br2.get_ip6_address(config.ADDRESS_TYPE.BACKBONE_GUA), backbone=True, ttl=10))
140        self.simulator.go(5)
141
142        self.collect_ipaddrs()
143        self.collect_rloc16s()
144        self.collect_rlocs()
145        self.collect_leader_aloc(BR_1)
146        self.collect_extra_vars()
147
148    def verify(self, pv: pktverify.packet_verifier.PacketVerifier):
149        pkts = pv.pkts
150        vars = pv.vars
151        pv.summary.show()
152
153        # Ensure the topology is formed correctly
154        pv.verify_attached('TD', 'BR_1')
155        pv.verify_attached('BR_2')
156
157        # 1. TD Registers for multicast address, MA1, at BR_1.
158        # TD unicasts an MLR.req CoAP request to BR_1 as
159        # "coap://[<BR_1 RLOC or PBBR ALOC>]:MM/n/mr".
160        # The payload contains "IPv6Address TLV: MA1".
161        pkts.filter_wpan_src64(vars['TD']) \
162            .filter_ipv6_2dsts(vars['BR_1_RLOC'], PBBR_ALOC) \
163            .filter_coap_request('/n/mr') \
164            .filter(lambda p: p.thread_meshcop.tlv.ipv6_addr == [MA1]) \
165            .must_next()
166
167        # 3. BR_1 responds to the multicast registration.
168        # BR_1 unicasts an MLR.rsp CoAP response to TD as "2.04 changed".
169        # The payload contains "Status TLV: ST_MLR_SUCCESS".
170        pkts.copy().filter_wpan_src64(vars['BR_1']) \
171            .filter_ipv6_dst(vars['TD_RLOC']) \
172            .filter_coap_ack('/n/mr') \
173            .filter(lambda p: p.thread_nm.tlv.status == 0) \
174            .must_next()
175
176        # 3a. BR_2 does not respond to the multicast registration.
177        pkts.filter_wpan_src64(vars['BR_2']) \
178            .filter_ipv6_dst(vars['TD_RLOC']) \
179            .filter_coap_ack('/n/mr') \
180            .must_not_next()
181
182        # 4. BR_1 informs other BBRs on the network of multicast registration.
183        # BR_1 multicasts a BMLR.ntf CoAP request to the Backbone Link including
184        # to BR_2, as follows
185        pkts.filter_eth_src(vars['BR_1_ETH']) \
186            .filter_ipv6_dst(config.ALL_NETWORK_BBRS_ADDRESS) \
187            .filter_coap_request('/b/bmr') \
188            .filter(lambda
189                        p: p.thread_meshcop.tlv.ipv6_addr == [MA1] and
190                           p.thread_bl.tlv.timeout == MLR_NOTIFICATION_TIMEOUT) \
191            .must_next()
192
193        # 6. BR_1 multicasts an MLDv2 message to start listening to MA1.
194        # BR_1 multicasts an MLDv2 message of type “Version 2 Multicast Listener
195        # Report” (see [RFC 3810] Section 5.2). Where:
196        # Nr of Mcast Address at least 1 Records (M): Multicast Address Record
197        # The Multicast Address Record contains the following:
198        # Record Type: 4 (CHANGE_TO_EXCLUDE_MODE)
199        # Number of Sources (N): 0
200        # Multicast Address: MA1
201        # TODO: Implement this verification
202
203        # 7. Host sends a ping packet to the multicast address, MA1.
204        _pkt = pkts.filter_eth_src(vars['Host_ETH']) \
205            .filter_ipv6_dst(MA1) \
206            .filter_ping_request() \
207            .must_next()
208
209        # 8. BR_2 does not forward the ping packet with multicast address MA1 to
210        # its Thread Network.
211        pkts.filter_wpan_src64(vars['BR_2']) \
212            .filter_AMPLFMA(mpl_seed_id=(vars['BR_2_RLOC'])) \
213            .filter_ping_request(identifier=_pkt.icmpv6.echo.identifier) \
214            .must_not_next()
215
216        # 9. BR_1 forwards the ping packet to its Thread Network.
217        # BR_1 forwards the ping packet with multicast address, MA1, to its
218        # Thread Network encapsulated in an MPL packet, where:
219        # MPL Seed ID:  If Source outer IP header = BR_1 RLOC, SeedID length = 0
220        #               Else, SeedID length = 1, and Seed ID = BR_1 RLOC16
221        pkts.filter_wpan_src64(vars['BR_1']) \
222            .filter_AMPLFMA(mpl_seed_id=(vars['BR_1_RLOC'])) \
223            .filter_ping_request(identifier=_pkt.icmpv6.echo.identifier) \
224            .must_next()
225
226        # 10. TD receives the multicast ping packet and sends a ping response
227        # packet back to Host.
228        # TD receives the MPL packet containing an encapsulated ping packet to
229        # MA1, sent by Host, and unicasts a ping response packet back to Host.
230        pkts.filter_wpan_src64(vars['TD']) \
231            .filter_ipv6_dst(_pkt.ipv6.src) \
232            .filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier) \
233            .must_next()
234
235        # 11. Host sends a ping packet to the multicast address, MA2.
236        _pkt = pkts.filter_eth_src(vars['Host_ETH']) \
237            .filter_ipv6_dst(MA2) \
238            .filter_ping_request() \
239            .must_next()
240
241        # 12. BR_2 does not forward the ping packet with multicast address, MA2,
242        # to the Thread Network in whatever way.
243        pkts.filter_wpan_src64(vars['BR_2']) \
244            .filter_AMPLFMA(mpl_seed_id=(vars['BR_2_RLOC'])) \
245            .filter_ping_request(identifier=_pkt.icmpv6.echo.identifier) \
246            .must_not_next()
247
248        # 13. BR_1 does not forward the ping packet with multicast address, MA2,
249        # to the Thread Network in whatever way.
250        pkts.filter_wpan_src64(vars['BR_1']) \
251            .filter_AMPLFMA(mpl_seed_id=(vars['BR_1_RLOC'])) \
252            .filter_ping_request(identifier=_pkt.icmpv6.echo.identifier) \
253            .must_not_next()
254
255        # 14. Host sends a ping packet to the multicast address, MA1g.
256        _pkt = pkts.filter_eth_src(vars['Host_ETH']) \
257            .filter_ipv6_dst(MA1g) \
258            .filter_ping_request() \
259            .must_next()
260
261        # 15. BR_2 does not forward the ping packet with multicast address MA1g,
262        # to the Thread Network in whatever way.
263        pkts.filter_wpan_src64(vars['BR_2']) \
264            .filter_AMPLFMA(mpl_seed_id=(vars['BR_2_RLOC'])) \
265            .filter_ping_request(identifier=_pkt.icmpv6.echo.identifier) \
266            .must_not_next()
267
268        # 16. BR_1 does not forward the ping packet with multicast address MA1g,
269        # to its Thread Network in whatever way.
270        pkts.filter_wpan_src64(vars['BR_1']) \
271            .filter_AMPLFMA(mpl_seed_id=(vars['BR_1_RLOC'])) \
272            .filter_ping_request(identifier=_pkt.icmpv6.echo.identifier) \
273            .must_not_next()
274
275        # 17. Host sends a ping packet to the BR_2's global unicast address, Gg.
276        _pkt = pkts.filter_eth_src(vars['Host_ETH']) \
277            .filter_ipv6_dst(vars['BR_2_BGUA']) \
278            .filter_ping_request() \
279            .must_next()
280
281        # 18. BR_2 receives and provides the ping response.
282        # BR_2 Must send back the ping response to the Host.
283        pkts.filter_eth_src(vars['BR_2_ETH']) \
284            .filter_ipv6_dst(vars['Host_BGUA']) \
285            .filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier) \
286            .must_next()
287
288
289if __name__ == '__main__':
290    unittest.main()
291