From ecd4b01422f75a5fb7c7a2f2ab57259ee177dc84 Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Tue, 24 Feb 2026 14:54:51 -0600 Subject: [PATCH] [nexus] add test 9.2.7 and fix pktverify timestamp parsing (#12535) This commit adds Nexus test 9.2.7 and improves the pktverify framework to correctly handle Thread timestamp parsing. The implementation includes: - tests/nexus/test_9_2_7.cpp: - C++ execution logic using direct core method calls. - Implements a real Commissioner session to correctly authorize Dataset updates. - Documents a deviation in Step 5 where the Active Timestamp is set to 15s (instead of 20s) to satisfy strictly increasing timestamp requirements for subsequent steps. - tests/nexus/verify_9_2_7.py: - Python verification with strict field checks for timestamps and timers. - Handles interleaved traffic using save_index(). - Updates verification logic to accommodate timer decrements. - Integration: - tests/nexus/CMakeLists.txt: Added nexus_9_2_7 target. - tests/nexus/run_nexus_tests.sh: Added 9_2_7 to default test list. The pktverify fixes include: - tests/scripts/thread-cert/pktverify/layer_fields.py: - Added _thread_timestamp parser to handle raw 8-byte Thread timestamps, avoiding fragility and timezone issues associated with date string parsing. - Fixed _auto parser to use calendar.timegm() for UTC date strings instead of time.mktime(), ensuring correct epoch values regardless of the local system timezone. - Mapped active and pending timestamp fields to use the new _thread_timestamp parser. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/run_nexus_tests.sh | 1 + tests/nexus/test_9_2_7.cpp | 681 ++++++++++++++++++ tests/nexus/verify_9_2_7.py | 337 +++++++++ .../thread-cert/pktverify/layer_fields.py | 20 +- 5 files changed, 1035 insertions(+), 5 deletions(-) create mode 100644 tests/nexus/test_9_2_7.cpp create mode 100644 tests/nexus/verify_9_2_7.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 0f66f9fb3..2e4e5b7d3 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -200,6 +200,7 @@ ot_nexus_test(9_2_3 "cert;nexus") ot_nexus_test(9_2_4 "cert;nexus") ot_nexus_test(9_2_5 "cert;nexus") ot_nexus_test(9_2_6 "cert;nexus") +ot_nexus_test(9_2_7 "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 d3a731f56..46c7bb238 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -136,6 +136,7 @@ DEFAULT_TESTS=( "9_2_4" "9_2_5" "9_2_6" + "9_2_7" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_9_2_7.cpp b/tests/nexus/test_9_2_7.cpp new file mode 100644 index 000000000..65d64ba60 --- /dev/null +++ b/tests/nexus/test_9_2_7.cpp @@ -0,0 +1,681 @@ +/* + * 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 "meshcop/commissioner.hpp" +#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 = 20 * 1000; + +/** + * Time to advance for a node to join a network, in milliseconds. + */ +static constexpr uint32_t kJoinTime = 20 * 1000; + +/** + * Time to advance for a commissioner to become active, in milliseconds. + */ +static constexpr uint32_t kPetitionTime = 15 * 1000; + +/** + * Time to wait for a response, in milliseconds. + */ +static constexpr uint32_t kResponseTime = 15000; + +/** + * Time to wait for ICMPv6 Echo response, in milliseconds. + */ +static constexpr uint32_t kEchoTimeout = 5000; + +/** + * Delay timer value in seconds (60 minutes). + */ +static constexpr uint32_t kDelayTimerStep11 = 60 * 60; + +/** + * Delay timer value in seconds (1 minute). + */ +static constexpr uint32_t kDelayTimerStep17 = 60; + +/** + * Active Timestamp for Leader. + */ +static constexpr uint64_t kActiveTimestampLeader = 10; + +/** + * Active Timestamp for Router. + */ +static constexpr uint64_t kActiveTimestampStep5 = 15; +static constexpr uint64_t kActiveTimestampRouter = 20; + +/** + * Pending Timestamp for Router. + */ +static constexpr uint64_t kPendingTimestampRouter = 30; + +/** + * Pending Timestamp for Commissioner. + */ +static constexpr uint64_t kPendingTimestampCommissioner = 40; + +/** + * Active Timestamp for Commissioner. + */ +static constexpr uint64_t kActiveTimestampCommissioner = 80; + +/** + * Router Partition Weight. + */ +static constexpr uint8_t kRouterWeight = 47; + +/** + * PAN ID for Step 17. + */ +static constexpr uint16_t kPanIdStep17 = 0xafce; + +/** + * Secondary Channel. + */ +static constexpr uint8_t kSecondaryChannel = 12; + +/** + * Fixed Network Key for stable decryption. + */ +static constexpr uint8_t kNetworkKey[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}; + +void Test9_2_7(void) +{ + /** + * 9.2.7 Commissioning – Delay Timer Management + * + * 9.2.7.1 Topology + * - NOTE: Two sniffers are required to run this test case! + * - Set on Leader: Active Timestamp = 10s + * - Set on Router: Active Timestamp = 20s, Pending Timestamp = 30s + * - At the start of the test, the Router has a current Pending Operational Dataset with a delay timer set to 60 + * minutes. + * - Router Partition Weight is configured to a value of 47, to make it always lower than the Leader's weight. + * - Initially, there is no link between the Leader and the Router. + * + * 9.2.7.2 Purpose & Description + * The purpose of this test case is to verify that if the Leader receives a Pending Operational Dataset with a + * newer Pending Timestamp, it resets the running delay timer, installs the new Pending Operational Dataset, and + * disseminates the new Commissioning information in the network. + * + * Spec Reference | V1.1 Section | V1.3.0 Section + * ----------------------------------------|--------------|--------------- + * Updating the Active Operational Dataset | 8.7.4 | 8.7.4 + */ + + Core nexus; + + Node &leader = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + Node &commissioner = nexus.CreateNode(); + + leader.SetName("LEADER"); + router.SetName("ROUTER"); + commissioner.SetName("COMMISSIONER"); + + nexus.AdvanceTime(0); + + Instance::SetLogLevel(kLogLevelNote); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: All"); + + /** + * Step 1: All + * - Description: Ensure topology is formed correctly. + * - Pass Criteria: N/A + */ + + leader.AllowList(commissioner); + commissioner.AllowList(leader); + + { + MeshCoP::Dataset::Info datasetInfo; + + datasetInfo.Clear(); + datasetInfo.Set(reinterpret_cast(kNetworkKey)); + datasetInfo.mActiveTimestamp.mSeconds = kActiveTimestampLeader; + datasetInfo.mActiveTimestamp.mTicks = 0; + datasetInfo.mComponents.mIsActiveTimestampPresent = true; + datasetInfo.mComponents.mIsNetworkKeyPresent = true; + + MeshCoP::NetworkName networkName; + SuccessOrQuit(networkName.Set("Nexus-9-2-7")); + datasetInfo.Set(networkName); + + datasetInfo.Set(0x1234); + datasetInfo.Set(11); + + leader.Get().SaveLocal(datasetInfo); + } + + leader.Get().Up(); + SuccessOrQuit(leader.Get().Start()); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(leader.Get().IsLeader()); + + commissioner.Join(leader); + nexus.AdvanceTime(kJoinTime); + VerifyOrQuit(commissioner.Get().IsAttached()); + + nexus.AdvanceTime(20000); + + // Start commissioner session + SuccessOrQuit(commissioner.Get().Start(nullptr, nullptr, nullptr)); + nexus.AdvanceTime(kPetitionTime); + VerifyOrQuit(commissioner.Get().IsActive()); + + Log("Step 2: Harness"); + + /** + * Step 2: Harness + * - Description: Enable link between the DUT and Router. + * - Pass Criteria: N/A + */ + + leader.AllowList(router); + router.AllowList(leader); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: Router"); + + /** + * Step 3: Router + * - Description: Automatically attaches to the Leader (DUT). Within the MLE Child ID Request of the attach + * process, it includes the new active and pending timestamps. + * - Pass Criteria: N/A + */ + + router.Get().SetLeaderWeight(kRouterWeight); + + router.Join(leader); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Leader (DUT)"); + + /** + * Step 4: Leader (DUT) + * - Description: Automatically sends MLE Child ID Response to the Router. + * - Pass Criteria: The DUT MUST send a unicast MLE Child ID Response to the Router, including the following TLVs: + * - Active Operational Dataset TLV: + * - Channel TLV + * - Channel Mask TLV + * - Extended PAN ID TLV + * - Network Master Key TLV + * - Network Mesh-Local Prefix TLV + * - Network Name TLV + * - PAN ID TLV + * - PSKc TLV + * - Security Policy TLV + * - Active Timestamp TLV: 10s + */ + + nexus.AdvanceTime(kJoinTime); + VerifyOrQuit(router.Get().IsAttached()); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: Router"); + + /** + * Step 5: Router + * - Description: Harness instructs the Router to send a MGMT_ACTIVE_SET.req to the DUT’s Anycast or Router + * Locator: + * - CoAP Request URI: coap://[]:MM/c/as + * - CoAP Payload: < Commissioner Session ID TLV not present>, Active Timestamp TLV : 20s, Active Operational + * Dataset TLV: all parameters in Active Dataset + * - The Leader Anycast Locator uses the Mesh local prefix with an IID of 0000:00FF:FE00:FC00. + * - Pass Criteria: N/A + */ + + { + Tmf::Agent &agent = router.Get(); + Coap::Message *message; + MeshCoP::Dataset dataset; + MeshCoP::Dataset::Info datasetInfo; + + message = agent.NewPriorityConfirmablePostMessage(ot::kUriActiveSet); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(router.Get().Read(dataset)); + dataset.ConvertTo(datasetInfo); + + { + MeshCoP::Timestamp timestamp; + // Deviation from spec: Step 5 uses 15 instead of 20 to ensure it is older than the timestamp in Step 11. + timestamp.SetSeconds(kActiveTimestampStep5); + timestamp.SetTicks(0); + datasetInfo.Set(timestamp); + } + + // We use a fresh dataset object to encode the modified datasetInfo + MeshCoP::Dataset updatedDataset; + SuccessOrQuit(updatedDataset.WriteTlvsFrom(datasetInfo)); + SuccessOrQuit(message->AppendBytes(updatedDataset.GetBytes(), updatedDataset.GetLength())); + + Tmf::MessageInfo messageInfo(router.GetInstance()); + messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc(); + SuccessOrQuit(agent.SendMessage(*message, messageInfo)); + } + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: Leader (DUT)"); + + /** + * Step 6: Leader (DUT) + * - Description: Automatically sends a MGMT_ACTIVE_SET.rsp to the Router. + * - Pass Criteria: The DUT MUST send MGMT_ACTIVE_SET.rsp to the Router with the following format: + * - CoAP Response Code: 2.04 Changed + * - CoAP Payload: State TLV (value = Accept) + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 7: Leader (DUT)"); + + /** + * Step 7: Leader (DUT) + * - Description: Automatically multicasts a MLE Data Response with the new information. + * - Pass Criteria: The DUT MUST send MLE Data Response to the Link-Local All Nodes multicast address (FF02::1), + * including the following TLVs: + * - Source Address TLV + * - Leader Data TLV + * - Data Version field incremented + * - Stable Version field incremented + * - Active Timestamp TLV: 15s + * - Network Data TLV: + * - Commissioner Data TLV: + * - Stable flag set to 0 + * - Commissioner Session ID TLV + * - Border Agent Locator TLV + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 8: Leader (DUT)"); + + /** + * Step 8: Leader (DUT) + * - Description: Automatically sends MGMT_DATASET_CHANGED.ntf to the Commissioner. + * - Pass Criteria: The DUT MUST send MGMT_DATASET_CHANGED.ntf to the Commissioner with the following format: + * - CoAP Request URI: coap://[Commissioner]:MM/c/dc + * - CoAP Payload: + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 9: Router"); + + /** + * Step 9: Router + * - Description: Automatically sends a unicast MLE Data Request to the Leader (DUT) with its current Active + * Timestamp. + * - Pass Criteria: N/A + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 10: Leader (DUT)"); + + /** + * Step 10: Leader (DUT) + * - Description: Automatically sends a unicast MLE Data Response to the Router with the new active timestamp and + * active operational dataset. + * - Pass Criteria: The DUT MUST send a unicast MLE Data Response to the Router, including the following TLVs: + * - Active Operational Dataset TLV + * - Channel TLV + * - Channel Mask TLV + * - Extended PAN ID TLV + * - Network Master Key TLV + * - Network Mesh-Local Prefix TLV + * - Network Name TLV + * - PAN ID TLV + * - PSKc TLV + * - Security Policy TLV + * - Active Timestamp TLV: 20s + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 11: Commissioner"); + + /** + * Step 11: Commissioner + * - Description: Harness instructs the Commissioner to send a MGMT_PENDING_SET.req to the DUT’s Anycast or Routing + * Locator: + * - CoAP Request URI: coap://[]:MM/c/ps + * - CoAP Payload: < Commissioner Session ID TLV not present>, Pending Timestamp TLV: 30s, Active Timestamp TLV: + * 20s, Delay Timer TLV + * - Pass Criteria: N/A + */ + + { + Tmf::Agent &agent = commissioner.Get(); + Coap::Message *message = agent.NewPriorityConfirmablePostMessage(ot::kUriPendingSet); + + VerifyOrQuit(message != nullptr); + + { + MeshCoP::Timestamp timestamp; + timestamp.SetSeconds(kPendingTimestampRouter); + timestamp.SetTicks(0); + SuccessOrQuit(Tlv::Append(*message, timestamp)); + } + + { + MeshCoP::Timestamp timestamp; + timestamp.SetSeconds(kActiveTimestampRouter); + timestamp.SetTicks(0); + SuccessOrQuit(Tlv::Append(*message, timestamp)); + } + + SuccessOrQuit(Tlv::Append(*message, kDelayTimerStep11 * 1000)); + + Tmf::MessageInfo messageInfo(commissioner.GetInstance()); + messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc(); + SuccessOrQuit(agent.SendMessage(*message, messageInfo)); + } + + // Wait for acceptance and retransmissions if needed + nexus.AdvanceTime(2 * kResponseTime); + + { + MeshCoP::Timestamp timestamp; + timestamp = leader.Get().GetTimestamp(); + Log("Pending Timestamp: %s, seconds: %llu", timestamp.IsValid() ? "valid" : "invalid", + (unsigned long long)timestamp.GetSeconds()); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 12: Leader (DUT)"); + + /** + * Step 12: Leader (DUT) + * - Description: Automatically sends MGMT_PENDING_SET.rsp to the Commissioner and incorporates the new pending + * dataset values. + * - Pass Criteria: The DUT MUST send MGMT_PENDING_SET.rsp to the Commissioner with the following format: + * - CoAP Response Code: 2.04 Changed + * - CoAP Payload: State TLV (value = Accept) + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 13: Leader (DUT)"); + + /** + * Step 13: Leader (DUT) + * - Description: Automatically multicasts a MLE Data Response with the new information. + * - Pass Criteria: The DUT MUST send MLE Data Response to the Link-Local All Nodes multicast address (FF02::1), + * including the following TLVs: + * - Source Address TLV + * - Leader Data TLV + * - Data Version field incremented + * - Stable Version field incremented + * - Network Data TLV: + * - Commissioner Data TLV: + * - Stable flag set to 0 + * - Commissioner Session ID TLV + * - Border Agent Locator TLV + * - Active Timestamp TLV + * - Pending Timestamp TLV + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 14: Leader (DUT)"); + + /** + * Step 14: Leader (DUT) + * - Description: Automatically sends a MGMT_DATASET_CHANGED.ntf to the Commissioner. + * - Pass Criteria: THE DUT MUST send MGMT_DATASET_CHANGED.ntf to the Commissioner with the following format: + * - CoAP Request URI: coap://[Commissioner]:MM/c/dc + * - CoAP Payload: + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 15: Router"); + + /** + * Step 15: Router + * - Description: Automatically sends a unicast MLE Data Request to the Leader (DUT) with a new active timestamp. + * - Pass Criteria: N/A + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 16: Leader (DUT)"); + + /** + * Step 16: Leader (DUT) + * - Description: Automatically sends a unicast MLE Data Response to the Router with a new active timestamp, new + * pending timestamp, and a new pending operational dataset. + * - Pass Criteria: The DUT MUST send a unicast MLE Data Response to the Router, which includes the following + * TLVs: + * - Pending Operational Dataset TLV: + * - Active Timestamp TLV + * - Channel TLV + * - Channel Mask TLV + * - Delay Timer TLV + * - Extended PAN ID TLV + * - Network Master Key TLV + * - Network Mesh-Local Prefix TLV + * - Network Name TLV + * - PAN ID TLV + * - PSKc TLV + * - Security Policy TLV + * - Active Timestamp TLV: 20s + * - Pending Timestamp TLV: 100s + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 17: Commissioner"); + + /** + * Step 17: Commissioner + * - Description: Harness instructs the Commissioner to send a MGMT_PENDING_SET.req to the Leader’s Anycast or + * Routing Locator: + * - CoAP Request URI: coap://[]:MM/c/ps + * - CoAP Payload: Valid Commissioner Session ID TLV, Pending Timestamp TLV: 40s, Active Timestamp TLV: 80s, + * Delay Timer TLV: 1min, Channel TLV: ‘Secondary’, PAN ID TLV: 0xAFCE + * - The Leader Anycast Locator uses the Mesh local prefix with an IID of 0000:00FF:FE00:FC00. + * - Pass Criteria: N/A + */ + { + Tmf::Agent &agent = commissioner.Get(); + Coap::Message *message = agent.NewPriorityConfirmablePostMessage(ot::kUriPendingSet); + uint16_t sessionId = commissioner.Get().GetSessionId(); + + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, sessionId)); + { + MeshCoP::Timestamp timestamp; + timestamp.SetSeconds(kPendingTimestampCommissioner); + timestamp.SetTicks(0); + SuccessOrQuit(Tlv::Append(*message, timestamp)); + } + { + MeshCoP::Timestamp timestamp; + timestamp.SetSeconds(kActiveTimestampCommissioner); + timestamp.SetTicks(0); + SuccessOrQuit(Tlv::Append(*message, timestamp)); + } + SuccessOrQuit(Tlv::Append(*message, kDelayTimerStep17 * 1000)); + SuccessOrQuit(Tlv::Append(*message, MeshCoP::ChannelTlvValue(0, kSecondaryChannel))); + SuccessOrQuit(Tlv::Append(*message, kPanIdStep17)); + + Tmf::MessageInfo messageInfo(commissioner.GetInstance()); + messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc(); + SuccessOrQuit(agent.SendMessage(*message, messageInfo)); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 18: Leader (DUT)"); + + /** + * Step 18: Leader (DUT) + * - Description: Automatically sends a MGMT_PENDING_SET.rsp to the Commissioner with Status = Accept. + * - Pass Criteria: The DUT MUST send MGMT_PENDING_SET.rsp to the Commissioner with the following format: + * - CoAP Response Code: 2.04 Changed + * - CoAP Payload: State TLV (value = Accept (0x01)) + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 19: Leader (DUT)"); + + /** + * Step 19: Leader (DUT) + * - Description: Automatically sends a multicast MLE Data Response. + * - Pass Criteria: The DUT MUST send a MLE Data Response to the Link-Local All Nodes multicast address (FF02::1), + * including the following TLVs: + * - Source Address TLV + * - Leader Data TLV + * - Data Version field incremented + * - Stable Version field incremented + * - Network Data TLV: + * - Commissioning Data TLV: + * - Stable flag set to 0 + * - Commissioner Session ID TLV + * - Border Agent Locator TLV + * - Active Timestamp TLV: 20s + * - Pending Timestamp TLV: 40s + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 20: Router"); + + /** + * Step 20: Router + * - Description: Automatically sends a unicast MLE Data Request to the DUT with the new active timestamp and + * pending timestamp: + * - Active Timestamp TLV: 20s + * - Pending Timestamp TLV: 100s + * - Pass Criteria: N/A + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 21: Leader (DUT)"); + + /** + * Step 21: Leader (DUT) + * - Description: Automatically sends a unicast MLE Data Response to the Router with the active Timestamp, the new + * pending timestamp and the current pending operational dataset. + * - Pass Criteria: The DUT MUST send a unicast MLE Data Response to the Router, which includes the following + * TLVs: + * - Pending Operational Dataset TLV: + * - Channel TLV + * - Active Timestamp TLV + * - Channel Mask TLV + * - Extended PAN ID TLV + * - Network Mesh-Local Prefix TLV + * - Network Master Key TLV + * - Network Name TLV + * - PAN ID TLV + * - PSKc TLV + * - Security Policy TLV + * - Delay Timer TLV + * - Active Timestamp TLV: 20s + * - Pending Timestamp TLV: 40s + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 22: All"); + + /** + * Step 22: All + * - Description: Verify that after 60 seconds, the Thread network moves to the Secondary channel, with PAN ID: + * 0xAFCE. + * - Pass Criteria: N/A + */ + + nexus.AdvanceTime(100000); + + VerifyOrQuit(leader.Get().GetPanId() == kPanIdStep17); + VerifyOrQuit(leader.Get().GetPanChannel() == kSecondaryChannel); + + VerifyOrQuit(router.Get().GetPanId() == kPanIdStep17); + VerifyOrQuit(router.Get().GetPanChannel() == kSecondaryChannel); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 23: All"); + + /** + * Step 23: All + * - Description: Verify connectivity by sending an ICMPv6 Echo Request to the DUT mesh local address. + * - Pass Criteria: The DUT MUST respond with an ICMPv6 Echo Reply. + */ + + nexus.SendAndVerifyEchoRequest(router, leader.Get().GetMeshLocalEid(), 0, 0, kEchoTimeout); + + nexus.SaveTestInfo("test_9_2_7.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::Test9_2_7(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_9_2_7.py b/tests/nexus/verify_9_2_7.py new file mode 100644 index 000000000..3a373819d --- /dev/null +++ b/tests/nexus/verify_9_2_7.py @@ -0,0 +1,337 @@ +#!/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 + +# Active Timestamp for Leader. +ACTIVE_TIMESTAMP_LEADER = 10 + +# Active Timestamp for Step 5. +# Deviation from spec: Step 5 uses 15 instead of 20 to ensure it is older than the timestamp in Step 11. +ACTIVE_TIMESTAMP_STEP_5 = 15 + +# Active Timestamp for Router. +ACTIVE_TIMESTAMP_ROUTER = 20 + +# Pending Timestamp for Router. +PENDING_TIMESTAMP_ROUTER = 30 + +# Pending Timestamp for Commissioner. +PENDING_TIMESTAMP_COMMISSIONER = 40 + +# Active Timestamp for Commissioner. +ACTIVE_TIMESTAMP_COMMISSIONER = 80 + +# Delay timer value in seconds (60 minutes). +DELAY_TIMER_STEP_11 = 3600 + +# Delay timer value in seconds (1 minute). +DELAY_TIMER_STEP_17 = 60 + +# PAN ID for Step 17. +PAN_ID_STEP_17 = 0xafce + +# Secondary Channel. +SECONDARY_CHANNEL = 12 + + +def verify(pv): + # 9.2.7 Commissioning – Delay Timer Management + # + # 9.2.7.1 Topology + # - NOTE: Two sniffers are required to run this test case! + # - Set on Leader: Active Timestamp = 10s + # - Set on Router: Active Timestamp = 20s, Pending Timestamp = 30s + # - At the start of the test, the Router has a current Pending Operational Dataset with a delay timer set to 60 + # minutes. + # - Router Partition Weight is configured to a value of 47, to make it always lower than the Leader's weight. + # - Initially, there is no link between the Leader and the Router. + # + # 9.2.7.2 Purpose & Description + # The purpose of this test case is to verify that if the Leader receives a Pending Operational Dataset with a + # newer Pending Timestamp, it resets the running delay timer, installs the new Pending Operational Dataset, and + # disseminates the new Commissioning information in the network. + # + # Spec Reference | V1.1 Section | V1.3.0 Section + # ----------------------------------------|--------------|--------------- + # Updating the Active Operational Dataset | 8.7.4 | 8.7.4 + + pkts = pv.pkts + pv.summary.show() + + LEADER = pv.vars['LEADER'] + ROUTER = pv.vars['ROUTER'] + COMMISSIONER = pv.vars['COMMISSIONER'] + + # Step 1: All + # - Description: Ensure topology is formed correctly. + # - Pass Criteria: N/A + print("Step 1: Ensure topology is formed correctly.") + + # Step 2: Harness + # - Description: Enable link between the DUT and Router. + # - Pass Criteria: N/A + print("Step 2: Enable link between the DUT and Router.") + + # Step 3: Router + # - Description: Automatically attaches to the Leader (DUT). Within the MLE Child ID Request of the attach + # process, it includes the new active and pending timestamps. + # - Pass Criteria: N/A + print("Step 3: Router attaches to the Leader.") + + # Step 4: Leader (DUT) + # - Description: Automatically sends MLE Child ID Response to the Router. + # - Pass Criteria: The DUT MUST send a unicast MLE Child ID Response to the Router, including the following TLVs: + # - Active Operational Dataset TLV: + # - Channel TLV + # - Channel Mask TLV + # - Extended PAN ID TLV + # - Network Master Key TLV + # - Network Mesh-Local Prefix TLV + # - Network Name TLV + # - PAN ID TLV + # - PSKc TLV + # - Security Policy TLV + # - Active Timestamp TLV: 10s + print("Step 4: Leader (DUT) sends MLE Child ID Response to the Router.") + pkts.filter_mle_cmd(consts.MLE_CHILD_ID_RESPONSE).\ + filter(lambda p: p.mle.tlv.active_tstamp == ACTIVE_TIMESTAMP_LEADER).\ + must_next() + + # Steps 5-8 can be interleaved + with pkts.save_index(): + # Step 5: Router + # - Description: Harness instructs the Router to send a MGMT_ACTIVE_SET.req to the DUT’s Anycast or Router + # Locator: + # - CoAP Request URI: coap://[]:MM/c/as + # - CoAP Payload: < Commissioner Session ID TLV not present>, Active Timestamp TLV : 15s, Active Operational + # Dataset TLV: all parameters in Active Dataset + # - The Leader Anycast Locator uses the Mesh local prefix with an IID of 0000:00FF:FE00:FC00. + # - Note: Deviation from spec: Step 5 uses 15 instead of 20 to ensure it is older than the timestamp in Step 11. + # - Pass Criteria: N/A + print("Step 5: Router sends a MGMT_ACTIVE_SET.req to the DUT.") + pkts.filter_coap_request(consts.MGMT_ACTIVE_SET_URI).\ + filter(lambda p: p.coap.tlv.active_timestamp == ACTIVE_TIMESTAMP_STEP_5).\ + must_next() + + # Step 6: Leader (DUT) + # - Description: Automatically sends a MGMT_ACTIVE_SET.rsp to the Router. + # - Pass Criteria: The DUT MUST send MGMT_ACTIVE_SET.rsp to the Router with the following format: + # - CoAP Response Code: 2.04 Changed + # - CoAP Payload: State TLV (value = Accept) + print("Step 6: Leader (DUT) sends a MGMT_ACTIVE_SET.rsp to the Router.") + pkts.filter_coap_ack(consts.MGMT_ACTIVE_SET_URI).\ + filter(lambda p: p.coap.tlv.state == 1).\ + must_next() + + with pkts.save_index(): + # Step 8: Leader (DUT) + # - Description: Automatically sends MGMT_DATASET_CHANGED.ntf to the Commissioner. + # - Pass Criteria: The DUT MUST send MGMT_DATASET_CHANGED.ntf to the Commissioner with the following format: + # - CoAP Request URI: coap://[Commissioner]:MM/c/dc + # - CoAP Payload: + print("Step 8: Leader (DUT) sends MGMT_DATASET_CHANGED.ntf to the Commissioner.") + pkts.filter_coap_request(consts.MGMT_DATASET_CHANGED_URI).\ + must_next() + + # Step 7: Leader (DUT) multicasts a MLE Data Response with the new information. + print("Step 7: Leader (DUT) multicasts a MLE Data Response with the new information.") + pkts.filter_LLANMA().\ + filter_mle_cmd(consts.MLE_DATA_RESPONSE).\ + must_next() + + # Step 9: Router + # - Description: Automatically sends a unicast MLE Data Request to the Leader (DUT) with its current Active + # Timestamp. + # - Pass Criteria: N/A + print("Step 9: Router sends a unicast MLE Data Request to the Leader.") + + # Step 10: Leader (DUT) + # - Description: Automatically sends a unicast MLE Data Response to the Router with the new active timestamp and + # active operational dataset. + # - Pass Criteria: The DUT MUST send a unicast MLE Data Response to the Router, including the following TLVs: + # - Active Operational Dataset TLV + # - Channel TLV + # - Channel Mask TLV + # - Extended PAN ID TLV + # - Network Master Key TLV + # - Network Mesh-Local Prefix TLV + # - Network Name TLV + # - PAN ID TLV + # - PSKc TLV + # - Security Policy TLV + # - Active Timestamp TLV: 15s + print("Step 10: Leader (DUT) sends a unicast MLE Data Response to the Router.") + pkts.filter_mle_cmd(consts.MLE_DATA_RESPONSE).\ + filter(lambda p: p.mle.tlv.active_tstamp == ACTIVE_TIMESTAMP_STEP_5).\ + must_next() + + # Steps 11-14 can be interleaved + with pkts.save_index(): + # Step 11: Commissioner + # - Description: Harness instructs the Commissioner to send a MGMT_PENDING_SET.req to the DUT’s Anycast or Routing + # Locator: + # - CoAP Request URI: coap://[]:MM/c/ps + # - CoAP Payload: < Commissioner Session ID TLV not present>, Pending Timestamp TLV: 30s, Active Timestamp TLV: + # 20s, Delay Timer TLV + # - Pass Criteria: N/A + print("Step 11: Commissioner sends a MGMT_PENDING_SET.req to the DUT.") + pkts.filter_coap_request(consts.MGMT_PENDING_SET_URI).\ + must_next() + + # Step 12: Leader (DUT) + # - Description: Automatically sends MGMT_PENDING_SET.rsp to the Commissioner and incorporates the new pending + # dataset values. + # - Pass Criteria: The DUT MUST send MGMT_PENDING_SET.rsp to the Commissioner with the following format: + # - CoAP Response Code: 2.04 Changed + # - CoAP Payload: State TLV (value = Accept) + print("Step 12: Leader (DUT) sends MGMT_PENDING_SET.rsp to the Router.") + pkts.filter_coap_ack(consts.MGMT_PENDING_SET_URI).\ + filter(lambda p: p.coap.tlv.state == 1).\ + must_next() + + with pkts.save_index(): + # Step 14: Leader (DUT) + # - Description: Automatically sends a MGMT_DATASET_CHANGED.ntf to the Commissioner. + # - Pass Criteria: THE DUT MUST send MGMT_DATASET_CHANGED.ntf to the Commissioner with the following format: + # - CoAP Request URI: coap://[Commissioner]:MM/c/dc + # - CoAP Payload: + print("Step 14: Leader (DUT) sends MGMT_DATASET_CHANGED.ntf to the Commissioner.") + pkts.filter_coap_request(consts.MGMT_DATASET_CHANGED_URI).\ + must_next() + + # Step 13: Leader (DUT) multicasts a MLE Data Response with the new information. + print("Step 13: Leader (DUT) multicasts a MLE Data Response.") + pkts.filter_LLANMA().\ + filter_mle_cmd(consts.MLE_DATA_RESPONSE).\ + must_next() + + # Step 15: Router + # - Description: Automatically sends a unicast MLE Data Request to the Leader (DUT) with a new active timestamp. + # - Pass Criteria: N/A + print("Step 15: Router sends a unicast MLE Data Request to the Leader.") + + # Step 16: Leader (DUT) + # - Description: Automatically sends a unicast MLE Data Response to the Router with a new active timestamp, new + # pending timestamp, and a new pending operational dataset. + # - Pass Criteria: The DUT MUST send a unicast MLE Data Response to the Router, which includes the following + # TLVs: + # - Pending Operational Dataset TLV: + # - Active Timestamp TLV + # - Channel TLV + # - Channel Mask TLV + # - Delay Timer TLV + # - Extended PAN ID TLV + # - Network Master Key TLV + # - Network Mesh-Local Prefix TLV + # - Network Name TLV + # - PAN ID TLV + # - PSKc TLV + # - Security Policy TLV + # - Active Timestamp TLV: 20s + # - Pending Timestamp TLV: 100s + print("Step 16: Leader (DUT) sends a unicast MLE Data Response to the Router.") + pkts.filter_mle_cmd(consts.MLE_DATA_RESPONSE).\ + filter(lambda p: p.mle.tlv.active_tstamp == ACTIVE_TIMESTAMP_STEP_5 and\ + p.mle.tlv.pending_tstamp == PENDING_TIMESTAMP_ROUTER and\ + DELAY_TIMER_STEP_11 * 1000 - 10000 <= p.thread_meshcop.tlv.delay_timer <= DELAY_TIMER_STEP_11 * 1000).\ + must_next() + + # Steps 17-21 can be interleaved + with pkts.save_index(): + # Step 17: Commissioner + # - Description: Harness instructs the Commissioner to send a MGMT_PENDING_SET.req to the Leader’s Anycast or + # Routing Locator: + # - CoAP Request URI: coap://[]:MM/c/ps + # - CoAP Payload: Valid Commissioner Session ID TLV, Pending Timestamp TLV: 40s, Active Timestamp TLV: 80s, + # Delay Timer TLV: 1min, Channel TLV: ‘Secondary’, PAN ID TLV: 0xAFCE + # - The Leader Anycast Locator uses the Mesh local prefix with an IID of 0000:00FF:FE00:FC00. + # - Pass Criteria: N/A + print("Step 17: Commissioner sends a MGMT_PENDING_SET.req to the Leader.") + pkts.filter_coap_request(consts.MGMT_PENDING_SET_URI).\ + must_next() + + # Step 18: Leader (DUT) + # - Description: Automatically sends a MGMT_PENDING_SET.rsp to the Commissioner with Status = Accept. + # - Pass Criteria: The DUT MUST send MGMT_PENDING_SET.rsp to the Commissioner with the following format: + # - CoAP Response Code: 2.04 Changed + # - CoAP Payload: State TLV (value = Accept (0x01)) + print("Step 18: Leader (DUT) sends a MGMT_PENDING_SET.rsp to the Commissioner.") + pkts.filter_coap_ack(consts.MGMT_PENDING_SET_URI).\ + filter(lambda p: p.coap.tlv.state == 1).\ + must_next() + + # Step 19: Leader (DUT) sends a multicast MLE Data Response. + print("Step 19: Leader (DUT) sends a multicast MLE Data Response.") + pkts.filter_LLANMA().\ + filter_mle_cmd(consts.MLE_DATA_RESPONSE).\ + must_next() + + # Step 20: Router + # - Description: Automatically sends a unicast MLE Data Request to the DUT with the new active timestamp and + # pending timestamp: + # - Active Timestamp TLV: 20s + # - Pending Timestamp TLV: 100s + # - Pass Criteria: N/A + print("Step 20: Router sends a unicast MLE Data Request to the DUT.") + + # Step 21: Leader (DUT) sends a unicast MLE Data Response to the Router. + print("Step 21: Leader (DUT) sends a unicast MLE Data Response to the Router.") + pkts.filter_mle_cmd(consts.MLE_DATA_RESPONSE).\ + filter(lambda p: p.mle.tlv.active_tstamp == ACTIVE_TIMESTAMP_STEP_5 and\ + p.mle.tlv.pending_tstamp == PENDING_TIMESTAMP_COMMISSIONER and\ + DELAY_TIMER_STEP_17 * 1000 - 10000 <= p.thread_meshcop.tlv.delay_timer <= DELAY_TIMER_STEP_17 * 1000).\ + must_next() + + # Step 22: All + # - Description: Verify that after 60 seconds, the Thread network moves to the Secondary channel, with PAN ID: + # 0xAFCE. + # - Pass Criteria: N/A + print("Step 22: Verify network moves to Secondary channel.") + + # Step 23: All + # - Description: Verify connectivity by sending an ICMPv6 Echo Request to the DUT mesh local address. + # - Pass Criteria: The DUT MUST respond with an ICMPv6 Echo Reply. + print("Step 23: Verify connectivity.") + _pkt = pkts.filter_ping_request().\ + must_next() + pkts.filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).\ + must_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify) diff --git a/tests/scripts/thread-cert/pktverify/layer_fields.py b/tests/scripts/thread-cert/pktverify/layer_fields.py index 6a2ae46c9..e6a11e6ed 100644 --- a/tests/scripts/thread-cert/pktverify/layer_fields.py +++ b/tests/scripts/thread-cert/pktverify/layer_fields.py @@ -26,6 +26,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # +import calendar import datetime import sys import time @@ -71,7 +72,7 @@ def _auto(v: Union[LayerFieldsContainer, LayerField]): # there are integer seconds applied in the test cases try: time_str = datetime.datetime.strptime(dv, "%b %d, %Y %H:%M:%S.%f000 %Z") - time_in_sec = time.mktime(time_str.utctimetuple()) + time_in_sec = calendar.timegm(time_str.utctimetuple()) return int(time_in_sec) except (ValueError, TypeError): pass @@ -99,6 +100,15 @@ def _auto(v: Union[LayerFieldsContainer, LayerField]): raise ValueError((v, v.get_default_value(), v.raw_value)) +def _thread_timestamp(v: Union[LayerFieldsContainer, LayerField]) -> int: + """parse the layer field as a Thread timestamp (8 bytes)""" + assert not isinstance(v, LayerFieldsContainer) or len(v.fields) == 1 + rv = v.raw_value + if rv and len(rv) == 16: + return int(rv, 16) >> 16 + return _auto(v) + + def _payload(v: Union[LayerFieldsContainer, LayerField]) -> bytearray: """parse the layer field as a bytearray""" assert not isinstance(v, LayerFieldsContainer) or len(v.fields) == 1 @@ -306,8 +316,8 @@ _LAYER_FIELDS = { 'mle.tlv.scan_mask.e': _auto, 'mle.tlv.version': _auto, 'mle.tlv.source_addr': _auto, - 'mle.tlv.active_tstamp': _auto, - 'mle.tlv.pending_tstamp': _auto, + 'mle.tlv.active_tstamp': _thread_timestamp, + 'mle.tlv.pending_tstamp': _thread_timestamp, 'mle.tlv.leader_data.partition_id': _auto, 'mle.tlv.leader_data.weighting': _auto, 'mle.tlv.leader_data.data_version': _auto, @@ -616,8 +626,8 @@ _LAYER_FIELDS = { 'thread_meshcop.tlv.udp_port': _list(_auto), 'thread_meshcop.tlv.ba_locator': _auto, 'thread_meshcop.tlv.jr_locator': _auto, - 'thread_meshcop.tlv.active_tstamp': _auto, - 'thread_meshcop.tlv.pending_tstamp': _auto, + 'thread_meshcop.tlv.active_tstamp': _thread_timestamp, + 'thread_meshcop.tlv.pending_tstamp': _thread_timestamp, 'thread_meshcop.tlv.delay_timer': _auto, 'thread_meshcop.tlv.ipv6_addr': _list(_ipv6_addr),