Files
openthread/tests/nexus/verify_1_2_MATN_TC_15.py
Jonathan Hui 12aa812cf0 [nexus] add MATN-TC-15 test case for Primary BBR change (#12703)
This commit implements the MATN-TC-15 test case in the Nexus simulation
framework to verify that a Thread End Device detects a change of Primary
Backbone Router (BBR) and triggers a re-registration of its multicast
groups.

Key implementation details include:
- Implementation of MATN-TC-15 in C++ simulating a topology with two
  Border Routers (BR_1 and BR_2), a Thread Router, and a Thread End
  Device (TD as DUT).
- Simulation of Primary BBR failover by stopping BR_1 and waiting for
  BR_2 to become the new Primary BBR.
- Addition of a Python verification script to validate:
  - Detection of Primary BBR change by the DUT.
  - Multicast Listener Registration (MLR.req) sent by the DUT to BR_2.
  - Correct forwarding of MLR.req and MLR.rsp by the intermediate
    Thread Router.
  - Successful registration response (MLR.rsp) from BR_2 to the DUT.
- Inclusion of the full test specification as inline comments in both
  the C++ and Python files.
- Registration of the new test case in tests/nexus/CMakeLists.txt and
  tests/nexus/run_nexus_tests.sh.
2026-03-17 05:09:59 -05:00

177 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
#
# Copyright (c) 2026, The OpenThread Authors.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
import sys
import os
import ipaddress
# Add the current directory to sys.path to find verify_utils
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(CUR_DIR)
import verify_utils
# Multicast Registration (MLR) URI
MLR_URI = '/n/mr'
# Status code for MLR success
ST_MLR_SUCCESS = 0
def verify(pv):
# 5.10.11 MATN-TC-15: Change of Primary BBR triggers a re-registration
#
# 5.10.11.1 Topology
# - BR_1
# - BR_2
# - Router
# - TD (DUT)
#
# 5.10.11.2 Purpose & Description
# The purpose of this test case is to verify that a Thread End Device detects a change of Primary BBR device and
# triggers a re-registration to its multicast groups.
#
# Spec Reference | V1.2 Section | V1.3.0 Section
# -----------------|--------------|---------------
# Multicast | 5.10.11 | 5.10.11
pkts = pv.pkts
vars = pv.vars
pv.summary.show()
MA1 = vars['MA1']
def is_br_rloc_or_aloc(addr, br_name):
"""Checks if an IPv6 address is a BR RLOC or the PBBR ALOC."""
try:
addr_obj = ipaddress.ip_address(str(addr))
iid = addr_obj.packed[8:]
except (ValueError, AttributeError):
return False
# Check for RLOC or ALOC pattern 0000:00ff:fe00:xxxx
if iid[:6] != b'\x00\x00\x00\xff\xfe\x00':
return False
locator = int.from_bytes(iid[6:], 'big')
# Check for PBBR ALOC (fc38)
if locator == 0xfc38:
return True
# Check for BR RLOC
br_rloc16 = vars.get(f'{br_name}_RLOC16')
if br_rloc16 is not None and locator == br_rloc16:
return True
return False
# Step 0
# - Device: N/A
# - Description: Topology formation – BR_1, BR_2, Router. Topology formation - TD (DUT). Boot the DUT. Configure
# the DUT to register multicast address MA1.
# - Pass Criteria:
# - N/A
print("Step 0: Topology formation and initial registration.")
# Verify initial registration at BR_1
pkts.filter_wpan_src64(vars['TD']).\
filter_coap_request(MLR_URI).\
filter(lambda p: p.coap.tlv.ipv6_address == [MA1]).\
filter(lambda p: is_br_rloc_or_aloc(p.ipv6.dst, 'BR_1')).\
must_next()
# Step 1
# - Device: BR_1
# - Description: Harness instructs the device to power down. Harness waits for BR_2 to become the Primary BBR.
# - Pass Criteria:
# - N/A
print("Step 1: BR_1 powers down, BR_2 becomes Primary.")
# Step 2
# - Device: TD (DUT)
# - Description: Automatically detects the Primary BBR change and registers for multicast address, MA1, at BR_2.
# - Pass Criteria:
# - The DUT MUST unicast an MLR.req CoAP request as follows: coap://[<BR_2 RLOC or PBBR ALOC>]:MM/n/mr
# - Where the payload contains: IPv6 Addresses TLV: MA1
print("Step 2: TD registers for MA1 at BR_2.")
pkts.filter_wpan_src64(vars['TD']).\
filter_coap_request(MLR_URI).\
filter(lambda p: p.coap.tlv.ipv6_address == [MA1]).\
filter(lambda p: is_br_rloc_or_aloc(p.ipv6.dst, 'BR_2')).\
must_next()
# Step 3
# - Device: Router
# - Description: Automatically forwards the registration request to BR_2.
# - Pass Criteria:
# - N/A
print("Step 3: Router forwards the registration request.")
pkts.filter_wpan_src64(vars['Router']).\
filter(lambda p: p.wpan.dst16 == vars['BR_2_RLOC16']).\
filter_coap_request(MLR_URI).\
filter(lambda p: p.coap.tlv.ipv6_address == [MA1]).\
filter(lambda p: is_br_rloc_or_aloc(p.ipv6.dst, 'BR_2')).\
must_next()
# Step 4
# - Device: BR_2
# - Description: Automatically unicasts an MLR.rsp CoAP response to TD as follows: 2.04 changed. Where the payload
# contains: Status TLV: 0 [ST_MLR_SUCCESS].
# - Pass Criteria:
# - N/A
print("Step 4: BR_2 sends an MLR.rsp.")
pkts.filter_wpan_src64(vars['BR_2']).\
filter(lambda p: p.wpan.dst16 == vars['Router_RLOC16']).\
filter_coap_ack(MLR_URI).\
filter(lambda p: p.coap.tlv.status == ST_MLR_SUCCESS).\
must_next()
# Step 5
# - Device: Router
# - Description: Automatically forwards the response to TD.
# - Pass Criteria:
# - N/A
print("Step 5: Router forwards the response.")
pkts.filter_wpan_src64(vars['Router']).\
filter_wpan_dst16(vars['TD_RLOC16']).\
filter_coap_ack(MLR_URI).\
filter(lambda p: p.coap.tlv.status == ST_MLR_SUCCESS).\
must_next()
# Step 6
# - Device: TD (DUT)
# - Description: Receives the CoAP registration response.
# - Pass Criteria:
# - The DUT MUST receive the CoAP response.
print("Step 6: TD receives the CoAP response.")
if __name__ == '__main__':
verify_utils.run_main(verify)