diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index d68bd6861..746b8078d 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -117,6 +117,7 @@ ot_nexus_test(5_1_1) ot_nexus_test(5_1_2) ot_nexus_test(5_1_3) ot_nexus_test(5_1_4) +ot_nexus_test(5_1_5) ot_nexus_test(border_agent) ot_nexus_test(border_agent_tracker) ot_nexus_test(discover_scan) diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index b0974edec..14034368b 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -52,6 +52,7 @@ DEFAULT_TESTS=( "5_1_2" "5_1_3" "5_1_4" + "5_1_5" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_5_1_5.cpp b/tests/nexus/test_5_1_5.cpp new file mode 100644 index 000000000..2b6312f0a --- /dev/null +++ b/tests/nexus/test_5_1_5.cpp @@ -0,0 +1,216 @@ +/* + * 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 "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 = 13 * 1000; + +/** + * Time to advance for a node to join as a child and upgrade to a router, in milliseconds. + */ +static constexpr uint32_t kAttachToRouterTime = 200 * 1000; + +/** + * Wait time in Step 2, in milliseconds. + * (200 = MAX_NEIGHBOR_AGE + INFINITE_COST_TIMEOUT + extra time) + */ +static constexpr uint32_t kStep2WaitTime = 200 * 1000; + +/** + * Wait time in Step 5, in milliseconds. + * (300 = MAX_NEIGHBOR_AGE + INFINITE_COST_TIMEOUT + ID_REUSE_DELAY + extra time) + */ +static constexpr uint32_t kStep5WaitTime = 300 * 1000; + +void Test5_1_5(void) +{ + /** + * 5.1.5 Router Address Timeout + * + * 5.1.5.1 Topology + * - Leader (DUT) + * - Router_1 + * + * 5.1.5.2 Purpose & Description + * The purpose of this test case is to verify that after deallocating a Router ID, the Leader (DUT) does not + * reassign the Router ID for at least ID_REUSE_DELAY seconds. + * + * Spec Reference | V1.1 Section | V1.3.0 Section + * --------------------------------------------|----------------|--------------- + * Router ID Management / Router ID Assignment | 5.9.9 / 5.9.10 | 5.9.9 / 5.9.10 + */ + + Core nexus; + + Node &leader = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + + leader.SetName("LEADER"); + router1.SetName("ROUTER_1"); + + nexus.AdvanceTime(0); + + Instance::SetLogLevel(kLogLevelInfo); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: All"); + + /** + * Step 1: All + * - Description: Verify topology is formed correctly + * - Pass Criteria: N/A + */ + + /** Use AllowList feature to restrict the topology. */ + leader.AllowList(router1); + router1.AllowList(leader); + + leader.Form(); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(leader.Get().IsLeader()); + + router1.Get().SetRouterSelectionJitter(1); + router1.Join(leader); + nexus.AdvanceTime(kAttachToRouterTime); + VerifyOrQuit(router1.Get().IsRouter()); + + uint8_t firstRouterId = Mle::RouterIdFromRloc16(router1.Get().GetRloc16()); + Log("Router_1 joined with Router ID: %u", firstRouterId); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: Router_1"); + + /** + * Step 2: Router_1 + * - Description: Harness silently powers-off Router_1 for 200 seconds. + * - (200 = MAX_NEIGHBOR_AGE + INFINITE_COST_TIMEOUT + extra time) + * - Extra time is added so Router_1 is brought back within ID_REUSE_DELAY interval + * - Pass Criteria: N/A + */ + router1.Get().Stop(); + nexus.AdvanceTime(kStep2WaitTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: Router_1"); + + /** + * Step 3: Router_1 + * - Description: Harness silently powers-on Router_1 after 200 seconds. + * - Router_1 automatically sends a link request, re-attaches and requests its original Router ID. + * - Pass Criteria: N/A + */ + IgnoreError(router1.Get().Start()); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Leader (DUT)"); + + /** + * Step 4: Leader (DUT) + * - Description: Automatically attaches Router_1 (Parent Response, Child ID Response, Address Solicit Response) + * - Pass Criteria: + * - The RLOC16 TLV in the Address Solicit Response message MUST contain a different Router ID than the one + * allocated in the original attach because ID_REUSE_DELAY interval has not timed out. + * - CoAP Response Code: 2.04 Changed + * - CoAP Payload: + * - Status TLV (value = Success) + * - RLOC16 TLV + * - Router Mask TLV + */ + nexus.AdvanceTime(kAttachToRouterTime); + VerifyOrQuit(router1.Get().IsRouter()); + + uint8_t secondRouterId = Mle::RouterIdFromRloc16(router1.Get().GetRloc16()); + Log("Router_1 re-joined with Router ID: %u", secondRouterId); + VerifyOrQuit(secondRouterId != firstRouterId, "Router ID was reused too early"); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: Router_1"); + + /** + * Step 5: Router_1 + * - Description: Harness silently powers-off Router_1 for 300 seconds. + * - (300 = MAX_NEIGHBOR_AGE + INFINITE_COST_TIMEOUT + ID_REUSE_DELAY + extra time) + * - Extra time is added to bring Router_1 back after ID_REUSE_DELAY interval + * - Pass Criteria: N/A + */ + router1.Get().Stop(); + nexus.AdvanceTime(kStep5WaitTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: Router_1"); + + /** + * Step 6: Router_1 + * - Description: Harness silently powers-on Router_1 after 300 seconds. + * - Router_1 reattaches and requests its most recent Router ID. + * - Pass Criteria: N/A + */ + IgnoreError(router1.Get().Start()); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 7: Leader (DUT)"); + + /** + * Step 7: Leader (DUT) + * - Description: Automatically attaches Router_1 (Parent Response, Child ID Response, Address Solicit Response) + * - Pass Criteria: + * - The RLOC16 TLV in the Address Solicit Response message MUST contain the requested Router ID + * - CoAP Response Code: 2.04 Changed + * - CoAP Payload: + * - Status TLV (value = Success) + * - RLOC16 TLV + * - Router Mask TLV + */ + nexus.AdvanceTime(kAttachToRouterTime); + VerifyOrQuit(router1.Get().IsRouter()); + + uint8_t thirdRouterId = Mle::RouterIdFromRloc16(router1.Get().GetRloc16()); + Log("Router_1 re-joined after ID_REUSE_DELAY with Router ID: %u", thirdRouterId); + VerifyOrQuit(thirdRouterId == secondRouterId, "Router ID was not reused after ID_REUSE_DELAY"); + + nexus.SaveTestInfo("test_5_1_5.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::Test5_1_5(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_5_1_5.py b/tests/nexus/verify_5_1_5.py new file mode 100644 index 000000000..679175dd6 --- /dev/null +++ b/tests/nexus/verify_5_1_5.py @@ -0,0 +1,180 @@ +#!/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 + +# 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 +from pktverify import consts + +# Bit offset of router ID in RLOC16 +ROUTER_ID_OFFSET = 10 + + +def verify(pv): + # 5.1.5 Router Address Timeout + # + # 5.1.5.1 Topology + # - Leader (DUT) + # - Router_1 + # + # 5.1.5.2 Purpose & Description + # The purpose of this test case is to verify that after deallocating a Router ID, the Leader (DUT) does not reassign + # the Router ID for at least ID_REUSE_DELAY seconds. + # + # Spec Reference | V1.1 Section | V1.3.0 Section + # --------------------------------------------|----------------|--------------- + # Router ID Management / Router ID Assignment | 5.9.9 / 5.9.10 | 5.9.9 / 5.9.10 + + pkts = pv.pkts + pv.summary.show() + + LEADER = pv.vars['LEADER'] + LEADER_RLOC16 = pv.vars['LEADER_RLOC16'] + ROUTER_1 = pv.vars['ROUTER_1'] + + # Step 1: All + # - Description: Verify topology is formed correctly + # - Pass Criteria: N/A + print("Step 1: Verify topology is formed correctly") + + # First attach of Router_1 + _pkt = pkts.filter_wpan_src64(ROUTER_1). \ + filter_wpan_dst16(LEADER_RLOC16). \ + filter_coap_request(consts.ADDR_SOL_URI). \ + must_next() + + _pkt_res = pkts.filter_wpan_src64(LEADER). \ + filter_wpan_dst16(_pkt.wpan.src16). \ + filter_coap_ack(consts.ADDR_SOL_URI). \ + must_next() + + first_router_id = _pkt_res.coap.tlv.rloc16 >> ROUTER_ID_OFFSET + print(f"Router_1 first Router ID: {first_router_id}") + + # Step 2: Router_1 + # - Description: Harness silently powers-off Router_1 for 160 seconds. + # - (160 = MAX_NEIGHBOR_AGE + INFINITE_COST_TIMEOUT + extra time) + # - Extra time is added so Router_1 is brought back within ID_REUSE_DELAY interval + # - Pass Criteria: N/A + print("Step 2: Router_1 powered off for 160 seconds") + + # Step 3: Router_1 + # - Description: Harness silently powers-on Router_1 after 160 seconds. + # - Router_1 automatically sends a link request, re-attaches and requests its original Router ID. + # - Pass Criteria: N/A + print("Step 3: Router_1 powered on after 160 seconds") + + # Step 4: Leader (DUT) + # - Description: Automatically attaches Router_1 (Parent Response, Child ID Response, Address Solicit Response) + # - Pass Criteria: + # - The RLOC16 TLV in the Address Solicit Response message MUST contain a different Router ID than the one + # allocated in the original attach because ID_REUSE_DELAY interval has not timed out. + # - CoAP Response Code: 2.04 Changed + # - CoAP Payload: + # - Status TLV (value = Success) + # - RLOC16 TLV + # - Router Mask TLV + print("Step 4: Leader (DUT) automatically attaches Router_1 and reassigns a different Router ID") + + _pkt = pkts.filter_wpan_src64(ROUTER_1). \ + filter_wpan_dst16(LEADER_RLOC16). \ + filter_coap_request(consts.ADDR_SOL_URI). \ + filter(lambda p: p.coap.tlv.rloc16 >> ROUTER_ID_OFFSET == first_router_id). \ + must_next() + + _pkt_res = pkts.filter_wpan_src64(LEADER). \ + filter_wpan_dst16(_pkt.wpan.src16). \ + filter_coap_ack(consts.ADDR_SOL_URI). \ + filter(lambda p: { + consts.NL_STATUS_TLV, + consts.NL_RLOC16_TLV, + consts.NL_ROUTER_MASK_TLV + } <= set(p.coap.tlv.type) and + p.coap.code == consts.COAP_CODE_ACK and + p.coap.tlv.status == 0). \ + must_next() + + second_router_id = _pkt_res.coap.tlv.rloc16 >> ROUTER_ID_OFFSET + print(f"Router_1 second Router ID: {second_router_id}") + assert second_router_id != first_router_id, "Router ID was reused too early" + + # Step 5: Router_1 + # - Description: Harness silently powers-off Router_1 for 300 seconds. + # - (300 = MAX_NEIGHBOR_AGE + INFINITE_COST_TIMEOUT + ID_REUSE_DELAY + extra time) + # - Extra time is added to bring Router_1 back after ID_REUSE_DELAY interval + # - Pass Criteria: N/A + print("Step 5: Router_1 powered off for 300 seconds") + + # Step 6: Router_1 + # - Description: Harness silently powers-on Router_1 after 300 seconds. + # - Router_1 reattaches and requests its most recent Router ID. + # - Pass Criteria: N/A + print("Step 6: Router_1 powered on after 300 seconds") + + # Step 7: Leader (DUT) + # - Description: Automatically attaches Router_1 (Parent Response, Child ID Response, Address Solicit Response) + # - Pass Criteria: + # - The RLOC16 TLV in the Address Solicit Response message MUST contain the requested Router ID + # - CoAP Response Code: 2.04 Changed + # - CoAP Payload: + # - Status TLV (value = Success) + # - RLOC16 TLV + # - Router Mask TLV + print("Step 7: Leader (DUT) automatically attaches Router_1 and reassigns the requested Router ID") + + _pkt = pkts.filter_wpan_src64(ROUTER_1). \ + filter_wpan_dst16(LEADER_RLOC16). \ + filter_coap_request(consts.ADDR_SOL_URI). \ + filter(lambda p: p.coap.tlv.rloc16 >> ROUTER_ID_OFFSET == second_router_id). \ + must_next() + + _pkt_res = pkts.filter_wpan_src64(LEADER). \ + filter_wpan_dst16(_pkt.wpan.src16). \ + filter_coap_ack(consts.ADDR_SOL_URI). \ + filter(lambda p: { + consts.NL_STATUS_TLV, + consts.NL_RLOC16_TLV, + consts.NL_ROUTER_MASK_TLV + } <= set(p.coap.tlv.type) and + p.coap.code == consts.COAP_CODE_ACK and + p.coap.tlv.status == 0). \ + must_next() + + third_router_id = _pkt_res.coap.tlv.rloc16 >> ROUTER_ID_OFFSET + print(f"Router_1 third Router ID: {third_router_id}") + assert third_router_id == second_router_id, "Router ID was not reused after ID_REUSE_DELAY" + + +if __name__ == '__main__': + verify_utils.run_main(verify)