From 12aa812cf0ba84fe9e17451c294c558cc0ec5a5e Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Tue, 17 Mar 2026 05:09:59 -0500 Subject: [PATCH] [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. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/run_nexus_tests.sh | 1 + tests/nexus/test_1_2_MATN_TC_15.cpp | 257 +++++++++++++++++++++++++++ tests/nexus/verify_1_2_MATN_TC_15.py | 176 ++++++++++++++++++ 4 files changed, 435 insertions(+) create mode 100644 tests/nexus/test_1_2_MATN_TC_15.cpp create mode 100644 tests/nexus/verify_1_2_MATN_TC_15.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 40f33a6d5..ff513e0c6 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -243,6 +243,7 @@ ot_nexus_test(1_2_MATN_TC_7 "cert;nexus") ot_nexus_test(1_2_MATN_TC_9 "cert;nexus") ot_nexus_test(1_2_MATN_TC_10 "cert;nexus") ot_nexus_test(1_2_MATN_TC_12 "cert;nexus") +ot_nexus_test(1_2_MATN_TC_15 "cert;nexus") # Misc tests ot_nexus_test(border_admitter "core;nexus") diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index 360e69ec4..0eadbb364 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -178,6 +178,7 @@ DEFAULT_TESTS=( "1_2_MATN_TC_9" "1_2_MATN_TC_10" "1_2_MATN_TC_12" + "1_2_MATN_TC_15" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_1_2_MATN_TC_15.cpp b/tests/nexus/test_1_2_MATN_TC_15.cpp new file mode 100644 index 000000000..dfac02159 --- /dev/null +++ b/tests/nexus/test_1_2_MATN_TC_15.cpp @@ -0,0 +1,257 @@ +/* + * 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. + */ + +#include + +#include + +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +/** + * Time to advance for a node to form a network and become leader, in milliseconds. + */ +static constexpr uint32_t kFormNetworkTime = 10 * 1000; + +/** + * Time to advance for a node to join as a router, in milliseconds. + */ +static constexpr uint32_t kAttachToRouterTime = 200 * 1000; + +/** + * Time to advance for the network to stabilize, in milliseconds. + */ +static constexpr uint32_t kStabilizationTime = 10 * 1000; + +/** + * Time to advance for the Primary BBR transition to occur, in milliseconds. + */ +static constexpr uint32_t kPbbrTransitionTime = 300 * 1000; + +/** + * Multicast address MA1 (admin-local). + */ +static const char kMA1[] = "ff04::1234:777a:1"; + +void TestMatnTc15(void) +{ + /** + * 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 + */ + + Core nexus; + Node &br1 = nexus.CreateNode(); + Node &br2 = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + Node &dut = nexus.CreateNode(); + Ip6::Address ma1; + + br1.SetName("BR_1"); + br2.SetName("BR_2"); + router.SetName("Router"); + dut.SetName("TD"); + + SuccessOrQuit(ma1.FromString(kMA1)); + + nexus.AdvanceTime(0); + + Instance::SetLogLevel(kLogLevelNote); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 0: Topology formation – BR_1, BR_2, Router. Topology formation - TD (DUT). Boot the DUT. Configure the " + "DUT to register multicast address MA1."); + + /** + * 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 + */ + + br1.AllowList(br2); + br1.AllowList(router); + + br2.AllowList(br1); + br2.AllowList(router); + + router.AllowList(br1); + router.AllowList(br2); + router.AllowList(dut); + + dut.AllowList(router); + + br1.Form(); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(br1.Get().IsLeader()); + + for (Node *br : {&br1, &br2}) + { + br->Get().Init(1, true); + br->Get().Init(); + SuccessOrQuit(br->Get().SetEnabled(true)); + br->Get().SetEnabled(true); + br->Get().SetRegistrationJitter(1); + } + + br2.Join(br1, Node::kAsFtd); + router.Join(br1, Node::kAsFtd); + nexus.AdvanceTime(kAttachToRouterTime); + + VerifyOrQuit(br2.Get().IsRouter()); + VerifyOrQuit(router.Get().IsRouter()); + + dut.Join(router, Node::kAsFed); + nexus.AdvanceTime(kAttachToRouterTime); + VerifyOrQuit(dut.Get().IsAttached()); + + SuccessOrQuit(dut.Get().SubscribeExternalMulticast(ma1)); + nexus.AdvanceTime(kStabilizationTime); + + // Add multicast variables to test info manually to ensure verify script sees them. + nexus.AddTestVar("MA1", kMA1); + + VerifyOrQuit(br1.Get().IsPrimary()); + VerifyOrQuit(!br2.Get().IsPrimary()); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: Harness instructs the device to power down. Harness waits for BR_2 to become the Primary BBR."); + + /** + * 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 + */ + br1.Get().Stop(); + nexus.AdvanceTime(kPbbrTransitionTime); + + for (int i = 0; i < 10; i++) + { + if (br2.Get().IsPrimary()) + { + break; + } + nexus.AdvanceTime(kStabilizationTime); + } + VerifyOrQuit(br2.Get().IsPrimary()); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: Automatically detects the Primary BBR change and registers for multicast address, MA1, at BR_2."); + + /** + * 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://[]:MM/n/mr + * - Where the payload contains: IPv6 Addresses TLV: MA1 + */ + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: Automatically forwards the registration request to BR_2."); + + /** + * Step 3 + * - Device: Router + * - Description: Automatically forwards the registration request to BR_2. + * - Pass Criteria: + * - N/A + */ + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Automatically unicasts an MLR.rsp CoAP response to TD as follows: 2.04 changed. Where the payload " + "contains: Status TLV: 0 [ST_MLR_SUCCESS]."); + + /** + * 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 + */ + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: Automatically forwards the response to TD."); + + /** + * Step 5 + * - Device: Router + * - Description: Automatically forwards the response to TD. + * - Pass Criteria: + * - N/A + */ + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: Receives the CoAP registration response."); + + /** + * Step 6 + * - Device: TD (DUT) + * - Description: Receives the CoAP registration response. + * - Pass Criteria: + * - The DUT MUST receive the CoAP response. + */ + nexus.AdvanceTime(kStabilizationTime); + + nexus.SaveTestInfo("test_1_2_MATN_TC_15.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestMatnTc15(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_1_2_MATN_TC_15.py b/tests/nexus/verify_1_2_MATN_TC_15.py new file mode 100644 index 000000000..b5240331d --- /dev/null +++ b/tests/nexus/verify_1_2_MATN_TC_15.py @@ -0,0 +1,176 @@ +#!/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://[]: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)