diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 2e4e5b7d3..13c52d999 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -201,6 +201,7 @@ 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") +ot_nexus_test(9_2_8 "cert;nexus") # Misc tests ot_nexus_test(border_admitter "core;nexus") diff --git a/tests/nexus/platform/nexus_pcap.cpp b/tests/nexus/platform/nexus_pcap.cpp index b7594bd61..73380433e 100644 --- a/tests/nexus/platform/nexus_pcap.cpp +++ b/tests/nexus/platform/nexus_pcap.cpp @@ -99,60 +99,47 @@ void Pcap::WriteFrame(const otRadioFrame &aFrame, uint64_t aTimeUs) OT_TOOL_PACKED_BEGIN struct TapHeader { - uint8_t mVersion; - uint8_t mReserved; + uint16_t mVersion; uint16_t mLength; } OT_TOOL_PACKED_END tapHeader; - OT_TOOL_PACKED_BEGIN - struct TapFcsTlv - { - uint16_t mType; - uint16_t mLength; - uint8_t mValue; - uint8_t mPadding[3]; - } OT_TOOL_PACKED_END fcsTlv; - - OT_TOOL_PACKED_BEGIN - struct TapChannelTlv - { - uint16_t mType; - uint16_t mLength; - uint8_t mPage; - uint16_t mChannel; - uint8_t mPadding[1]; - } OT_TOOL_PACKED_END channelTlv; - uint32_t tapLength; VerifyOrExit(mFile != nullptr); - tapLength = sizeof(tapHeader) + sizeof(fcsTlv) + sizeof(channelTlv); + tapLength = sizeof(tapHeader) + 8 + 8; + // Note: tshark expects specific lengths for TLVs. + // FCS TLV: Type(2), Length(2), Value(1), Padding(3) -> Total 8 bytes. Value length = 1. + // Channel TLV: Type(2), Length(2), Channel(2), Page(1), Padding(1) -> Total 8 bytes. Value length = 3. ClearAllBytes(recordHeader); recordHeader.mTsSec = LittleEndian::HostSwap(static_cast(aTimeUs / 1000000)); recordHeader.mTsUsec = LittleEndian::HostSwap(static_cast(aTimeUs % 1000000)); - recordHeader.mInclLen = LittleEndian::HostSwap(tapLength + aFrame.mLength); + recordHeader.mInclLen = LittleEndian::HostSwap(static_cast(tapLength + aFrame.mLength)); recordHeader.mOrigLen = recordHeader.mInclLen; VerifyOrExit(fwrite(&recordHeader, sizeof(recordHeader), 1, mFile) == 1, Close()); ClearAllBytes(tapHeader); - tapHeader.mVersion = LittleEndian::HostSwap(kTapVersion); + tapHeader.mVersion = LittleEndian::HostSwap(static_cast(kTapVersion)); tapHeader.mLength = LittleEndian::HostSwap(static_cast(tapLength)); VerifyOrExit(fwrite(&tapHeader, sizeof(tapHeader), 1, mFile) == 1, Close()); + // FCS TLV + uint8_t fcsTlv[8]; ClearAllBytes(fcsTlv); - fcsTlv.mType = LittleEndian::HostSwap(kTapFcsType); - fcsTlv.mLength = LittleEndian::HostSwap(kTapFcsLength); - fcsTlv.mValue = LittleEndian::HostSwap(kTapFcsValue); - VerifyOrExit(fwrite(&fcsTlv, sizeof(fcsTlv), 1, mFile) == 1, Close()); + LittleEndian::WriteUint16(kTapFcsType, &fcsTlv[0]); + LittleEndian::WriteUint16(kTapFcsLength, &fcsTlv[2]); + fcsTlv[4] = kTapFcsValue; + VerifyOrExit(fwrite(fcsTlv, sizeof(fcsTlv), 1, mFile) == 1, Close()); + // Channel TLV + uint8_t channelTlv[8]; ClearAllBytes(channelTlv); - channelTlv.mType = LittleEndian::HostSwap(kTapChannelType); - channelTlv.mLength = LittleEndian::HostSwap(kTapChannelLength); - channelTlv.mPage = LittleEndian::HostSwap(kTapChannelPage); - channelTlv.mChannel = LittleEndian::HostSwap(aFrame.mChannel); - VerifyOrExit(fwrite(&channelTlv, sizeof(channelTlv), 1, mFile) == 1, Close()); + LittleEndian::WriteUint16(kTapChannelType, &channelTlv[0]); + LittleEndian::WriteUint16(kTapChannelLength, &channelTlv[2]); + LittleEndian::WriteUint16(aFrame.mChannel, &channelTlv[4]); + channelTlv[6] = kTapChannelPage; + VerifyOrExit(fwrite(channelTlv, sizeof(channelTlv), 1, mFile) == 1, Close()); VerifyOrExit(fwrite(aFrame.mPsdu, aFrame.mLength, 1, mFile) == 1, Close()); VerifyOrExit(fflush(mFile) == 0, Close()); diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index 46c7bb238..74d6213e1 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -137,6 +137,7 @@ DEFAULT_TESTS=( "9_2_5" "9_2_6" "9_2_7" + "9_2_8" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_9_2_8.cpp b/tests/nexus/test_9_2_8.cpp new file mode 100644 index 000000000..929bcb4d7 --- /dev/null +++ b/tests/nexus/test_9_2_8.cpp @@ -0,0 +1,398 @@ +/* + * 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 "mac/data_poll_sender.hpp" +#include "meshcop/commissioner.hpp" +#include "meshcop/dataset_manager.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 = 13 * 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 = 5 * 1000; + +/** + * Time to wait for a response, in milliseconds. + */ +static constexpr uint32_t kResponseTime = 1000; + +/** + * Time to wait for the network to stabilize, in milliseconds. + */ +static constexpr uint32_t kStabilizeTime = 10 * 1000; + +/** + * Time for the delay timer, in milliseconds. + */ +static constexpr uint32_t kDelayTimer = 60 * 1000; + +/** + * Time to power down the DUT, in milliseconds. + */ +static constexpr uint32_t kPowerDownTime = 60 * 1000; + +/** + * Time to wait for reattachment after restart, in milliseconds. + */ +static constexpr uint32_t kReattachTime = 150 * 1000; + +/** + * PAN ID for the active dataset. + */ +static constexpr uint16_t kActivePanId = 0xFACE; + +/** + * PAN ID for the pending dataset. + */ +static constexpr uint16_t kPendingPanId = 0xAFCE; + +/** + * Primary channel. + */ +static constexpr uint8_t kPrimaryChannel = 11; + +/** + * Secondary channel. + */ +static constexpr uint8_t kSecondaryChannel = 12; + +/** + * Active Timestamp for the initial dataset. + */ +static constexpr uint64_t kInitialActiveTimestamp = 10; + +/** + * Active Timestamp for the pending set. + */ +static constexpr uint64_t kPendingActiveTimestamp = 70; + +/** + * Pending Timestamp for the pending set. + */ +static constexpr uint64_t kPendingTimestamp = 20; + +void Test9_2_8(void) +{ + /** + * 9.2.8 Commissioning - Persistent Active/Pending Operational Datasets + * + * 9.2.8.1 Topology + * - Commissioner + * - Leader + * - Router 1 (DUT) + * - MED 1 (DUT) + * - SED 1 (DUT) + * + * 9.2.8.2 Purpose & Description + * The purpose of this test case is to verify that after a reset, the DUT reattaches to the test network using + * parameters set in Active/Pending Operational Datasets. + * + * Spec Reference | V1.1 Section | V1.3.0 Section + * ----------------------------------------|--------------|--------------- + * Updating the Active Operational Dataset | 8.7.4 | 8.7.4 + */ + + Core nexus; + + Node &commissioner = nexus.CreateNode(); + Node &leader = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + Node &med1 = nexus.CreateNode(); + Node &sed1 = nexus.CreateNode(); + + commissioner.SetName("COMMISSIONER"); + leader.SetName("LEADER"); + router1.SetName("ROUTER_1"); + med1.SetName("MED_1"); + sed1.SetName("SED_1"); + + Instance::SetLogLevel(kLogLevelNote); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: All"); + + /** + * Step 1: All + * - Description: Ensure topology is formed correctly. + * - Pass Criteria: N/A. + */ + + commissioner.AllowList(leader); + leader.AllowList(commissioner); + + leader.AllowList(router1); + router1.AllowList(leader); + + leader.AllowList(med1); + med1.AllowList(leader); + + leader.AllowList(sed1); + sed1.AllowList(leader); + + { + MeshCoP::Dataset::Info dataset; + MeshCoP::Timestamp timestamp; + + SuccessOrQuit(dataset.GenerateRandom(leader.GetInstance())); + + timestamp.Clear(); + timestamp.SetSeconds(kInitialActiveTimestamp); + dataset.Set(timestamp); + dataset.Set(kPrimaryChannel); + dataset.Set(kActivePanId); + SuccessOrQuit(dataset.Update().Set("OpenThread")); + leader.Get().SaveLocal(dataset); + } + + leader.Get().Up(); + SuccessOrQuit(leader.Get().Start()); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(leader.Get().IsLeader()); + + commissioner.Join(leader, Node::kAsMed); + router1.Join(leader, Node::kAsFtd); + med1.Join(leader, Node::kAsMed); + sed1.Join(leader, Node::kAsSedWithFullNetData); + + SuccessOrQuit(sed1.Get().SetExternalPollPeriod(100)); + + nexus.AdvanceTime(kJoinTime); + VerifyOrQuit(commissioner.Get().IsAttached()); + VerifyOrQuit(router1.Get().IsAttached()); + VerifyOrQuit(med1.Get().IsAttached()); + VerifyOrQuit(sed1.Get().IsAttached()); + + SuccessOrQuit(commissioner.Get().Start(nullptr, nullptr, nullptr)); + nexus.AdvanceTime(kPetitionTime); + VerifyOrQuit(commissioner.Get().IsActive()); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: Commissioner"); + + /** + * Step 2: Commissioner + * - Description: Harness instructs device to send MGMT_PENDING_SET.req to the Leader Anycast or Routing Locator: + * - CoAP Request URI: coap://[]:MM/c/ps + * - CoAP Payload: + * - valid Commissioner Session ID TLV + * - Pending Timestamp TLV: 20s + * - Active Timestamp TLV: 70s + * - Delay Timer TLV: 60s + * - Channel TLV: ‘Secondary’ + * - PAN ID TLV: 0xAFCE + * - Pass Criteria: N/A. + */ + + { + MeshCoP::Dataset::Info pendingDataset; + MeshCoP::Timestamp timestamp; + + pendingDataset.Clear(); + + timestamp.Clear(); + timestamp.SetSeconds(kPendingTimestamp); + pendingDataset.Set(timestamp); + + timestamp.Clear(); + timestamp.SetSeconds(kPendingActiveTimestamp); + pendingDataset.Set(timestamp); + + pendingDataset.Set(kDelayTimer); + pendingDataset.Set(kSecondaryChannel); + pendingDataset.Set(kPendingPanId); + + SuccessOrQuit(commissioner.Get().SendSetRequest(pendingDataset, nullptr, 0, + nullptr, nullptr)); + } + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: Leader"); + + /** + * Step 3: Leader + * - Description: Automatically sends MGMT_PENDING_SET.rsq to the Commissioner. + * - Pass Criteria: + * - CoAP Response Code: 2.04 Changed + * - CoAP Payload: State TLV (value = Accept). + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Leader"); + + /** + * Step 4: Leader + * - Description: Automatically sends a multicast MLE Data Response to the DUT with the new network data, including + * the following TLVs: + * - Leader Data TLV: Data Version field incremented, Stable Version field incremented + * - Network Data TLV: Commissioner Data TLV (Stable flag set to 0, Border Agent Locator TLV, Commissioner + * Session ID TLV) + * - Active Timestamp TLV: 70s + * - Pending Timestamp TLV: 20s + * - Pass Criteria: N/A. + */ + + nexus.AdvanceTime(kStabilizeTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: DUT"); + + /** + * Step 5: DUT + * - Description: Automatically sends a MLE Data Request to request the full new network data. + * - Pass Criteria: The DUT MUST send a MLE Data Request to the Leader and include its current Active Timestamp. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: Leader"); + + /** + * Step 6: Leader + * - Description: Automatically sends a MLE Data Response including the following TLVs: Active Timestamp TLV, + * Pending Timestamp TLV, Active Operational Dataset TLV, Pending Operational Dataset TLV. Ensure enough time is + * allowed for network data to propagate to all devices. + * - Pass Criteria: N/A. + */ + + nexus.AdvanceTime(kStabilizeTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 7: User"); + + /** + * Step 7: User + * - Description: Powers down the DUT for 60 seconds. + * - Pass Criteria: N/A. + */ + + router1.Reset(); + med1.Reset(); + sed1.Reset(); + + nexus.AdvanceTime(kPowerDownTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 8: Leader, Commissioner"); + + /** + * Step 8: Leader, Commissioner + * - Description: After Delay Timer expires, the network moves to Channel = ‘Secondary’, PAN ID: 0xAFCE. + * - Pass Criteria: N/A. + */ + + nexus.AdvanceTime(kDelayTimer); + VerifyOrQuit(leader.Get().GetPanId() == kPendingPanId); + VerifyOrQuit(leader.Get().GetPanChannel() == kSecondaryChannel); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 9: User"); + + /** + * Step 9: User + * - Description: Restarts the DUT. + * - Pass Criteria: + * - The DUT MUST attempt to reattach by sending Parent Request using the parameters from Active Operational + * Dataset (Channel = ‘Primary’, PANID: 0xFACE). + * - The DUT MUST then attach using the parameters from the Pending Operational Dataset (Channel = ‘Secondary’, + * PANID: 0xAFCE). + */ + + router1.AllowList(leader); + med1.AllowList(leader); + sed1.AllowList(leader); + + SuccessOrQuit(router1.Get().SetDeviceMode(Mle::DeviceMode(Mle::DeviceMode::kModeRxOnWhenIdle | + Mle::DeviceMode::kModeFullThreadDevice | + Mle::DeviceMode::kModeFullNetworkData))); + SuccessOrQuit(med1.Get().SetDeviceMode( + Mle::DeviceMode(Mle::DeviceMode::kModeRxOnWhenIdle | Mle::DeviceMode::kModeFullNetworkData))); + SuccessOrQuit(sed1.Get().SetDeviceMode(Mle::DeviceMode(Mle::DeviceMode::kModeFullNetworkData))); + + router1.Get().Up(); + med1.Get().Up(); + sed1.Get().Up(); + + SuccessOrQuit(sed1.Get().SetExternalPollPeriod(100)); + + SuccessOrQuit(router1.Get().Start()); + SuccessOrQuit(med1.Get().Start()); + SuccessOrQuit(sed1.Get().Start()); + + nexus.AdvanceTime(kReattachTime); + + VerifyOrQuit(router1.Get().IsAttached()); + VerifyOrQuit(med1.Get().IsAttached()); + VerifyOrQuit(sed1.Get().IsAttached()); + + VerifyOrQuit(router1.Get().GetPanId() == kPendingPanId); + VerifyOrQuit(med1.Get().GetPanId() == kPendingPanId); + VerifyOrQuit(sed1.Get().GetPanId() == kPendingPanId); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 10: Commissioner"); + + /** + * Step 10: Commissioner + * - Description: Harness verifies connectivity by instructing the Commissioner to send an ICMPv6 Echo Request to + * the DUT mesh local address. + * - Pass Criteria: The DUT MUST respond with an ICMPv6 Echo Reply. + */ + + nexus.SendAndVerifyEchoRequest(commissioner, router1.Get().GetMeshLocalEid()); + nexus.SendAndVerifyEchoRequest(commissioner, med1.Get().GetMeshLocalEid()); + nexus.SendAndVerifyEchoRequest(commissioner, sed1.Get().GetMeshLocalEid(), 0, 64, 5000); + + nexus.SaveTestInfo("test_9_2_8.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::Test9_2_8(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_9_2_8.py b/tests/nexus/verify_9_2_8.py new file mode 100644 index 000000000..49bbac5ee --- /dev/null +++ b/tests/nexus/verify_9_2_8.py @@ -0,0 +1,222 @@ +#!/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 +from pktverify.null_field import nullField + + +def verify(pv): + # 9.2.8 Commissioning - Persistent Active/Pending Operational Datasets + # + # 9.2.8.1 Topology + # - Commissioner + # - Leader + # - Router 1 (DUT) + # - MED 1 (DUT) + # - SED 1 (DUT) + # + # 9.2.8.2 Purpose & Description + # The purpose of this test case is to verify that after a reset, the DUT reattaches to the test network using + # parameters set in Active/Pending Operational Datasets. + # + # 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'] + COMMISSIONER = pv.vars['COMMISSIONER'] + ROUTER_1 = pv.vars['ROUTER_1'] + MED_1 = pv.vars['MED_1'] + SED_1 = pv.vars['SED_1'] + + DUT_NAMES = ['ROUTER_1', 'MED_1', 'SED_1'] + DUT_EXTADDRS = [ROUTER_1, MED_1, SED_1] + + # Step 1: All + # - Description: Ensure topology is formed correctly. + # - Pass Criteria: N/A. + print("Step 1: Ensure topology is formed correctly.") + child_id_requests = pkts.filter_mle_cmd(consts.MLE_CHILD_ID_REQUEST) + last_index = pkts.index + for extaddr in DUT_EXTADDRS: + f = child_id_requests.copy().filter_wpan_src64(extaddr) + f.must_next() + last_index = max(last_index, f.index) + pkts.index = last_index + + # Step 2: Commissioner + # - Description: Harness instructs device to send MGMT_PENDING_SET.req to the Leader Anycast or Routing Locator: + # - CoAP Request URI: coap://[]:MM/c/ps + # - CoAP Payload: + # - valid Commissioner Session ID TLV + # - Pending Timestamp TLV: 20s + # - Active Timestamp TLV: 70s + # - Delay Timer TLV: 60s + # - Channel TLV: ‘Secondary’ + # - PAN ID TLV: 0xAFCE + # - Pass Criteria: N/A. + print("Step 2: Commissioner sends MGMT_PENDING_SET.req.") + pkts.filter_wpan_src64(COMMISSIONER). \ + filter_ipv6_dst(pv.vars['LEADER_ALOC']). \ + filter_coap_request(consts.MGMT_PENDING_SET_URI). \ + filter(lambda p: p.coap.tlv.active_timestamp == 70). \ + filter(lambda p: p.coap.tlv.pending_timestamp == 20). \ + filter(lambda p: p.coap.tlv.delay_timer == 60000). \ + must_next() + + # Step 3: Leader + # - Description: Automatically sends MGMT_PENDING_SET.rsq to the Commissioner. + # - Pass Criteria: + # - CoAP Response Code: 2.04 Changed + # - CoAP Payload: State TLV (value = Accept). + print("Step 3: Leader sends MGMT_PENDING_SET.rsq.") + pkts.filter(lambda p: p.coap is not nullField). \ + filter(lambda p: p.coap.code == consts.COAP_CODE_ACK). \ + filter(lambda p: p.coap.tlv is not nullField). \ + filter(lambda p: p.coap.tlv.state == consts.MESHCOP_ACCEPT). \ + must_next() + + # Step 4: Leader + # - Description: Automatically sends a multicast MLE Data Response to the DUT with the new network data, including + # the following TLVs: + # - Leader Data TLV: Data Version field incremented, Stable Version field incremented + # - Network Data TLV: Commissioner Data TLV (Stable flag set to 0, Border Agent Locator TLV, Commissioner + # Session ID TLV) + # - Active Timestamp TLV: 70s + # - Pending Timestamp TLV: 20s + # - Pass Criteria: N/A. + print("Step 4: Leader sends multicast MLE Data Response.") + pkts.filter_wpan_src64(LEADER). \ + filter_LLANMA(). \ + filter_mle_cmd(consts.MLE_DATA_RESPONSE). \ + filter(lambda p: p.mle.tlv.active_tstamp is not nullField). \ + filter(lambda p: p.mle.tlv.pending_tstamp == 20). \ + must_next() + + # Step 5 & 6: DUTs send MLE Data Request and Leader responds + # - Description: Automatically sends a MLE Data Request to request the full new network data. + # - Pass Criteria: The DUT MUST send a MLE Data Request to the Leader and include its current Active Timestamp. + # - Description: Automatically sends a MLE Data Response including the following TLVs: Active Timestamp TLV, + # Pending Timestamp TLV, Active Operational Dataset TLV, Pending Operational Dataset TLV. + print("Step 5 & 6: DUTs send MLE Data Request and Leader responds.") + requests = pkts.filter(lambda p: p.wpan.src64 is not nullField). \ + filter(lambda p: p.wpan.src64 in DUT_EXTADDRS). \ + filter_mle_cmd(consts.MLE_DATA_REQUEST). \ + filter(lambda p: p.mle.tlv.active_tstamp is not nullField) + + responses = pkts.filter_wpan_src64(LEADER). \ + filter(lambda p: p.wpan.dst64 is not nullField). \ + filter(lambda p: p.wpan.dst64 in DUT_EXTADDRS). \ + filter_mle_cmd(consts.MLE_DATA_RESPONSE). \ + filter(lambda p: p.mle.tlv.active_tstamp is not nullField). \ + filter(lambda p: p.mle.tlv.pending_tstamp == 20) + + # Verify each DUT sends a request and receives a response, and advance the packet index. + last_index = pkts.index + for dut in DUT_EXTADDRS: + f = requests.copy().filter_wpan_src64(dut) + f.must_next() + last_index = max(last_index, f.index) + + for dut in DUT_EXTADDRS: + f = responses.copy().filter_wpan_dst64(dut) + f.must_next() + last_index = max(last_index, f.index) + + # Advance the main pkts index to the end of Step 6 + pkts.index = last_index + + # Step 7: User + # - Description: Powers down the DUT for 60 seconds. + # - Pass Criteria: N/A. + print("Step 7: Powers down the DUT for 60 seconds.") + + # Step 8: Leader, Commissioner + # - Description: After Delay Timer expires, the network moves to Channel = ‘Secondary’, PAN ID: 0xAFCE. + # - Pass Criteria: N/A. + print("Step 8: Network moves to Channel = 'Secondary', PAN ID: 0xAFCE.") + pkts.filter_wpan_src64(LEADER). \ + filter_LLANMA(). \ + filter_mle_cmd(consts.MLE_DATA_RESPONSE). \ + filter(lambda p: p.mle.tlv.active_tstamp == 70). \ + filter(lambda p: p.mle.tlv.pending_tstamp is nullField). \ + must_next() + + # Step 9: User + # - Description: Restarts the DUT. + # - Pass Criteria: + # - The DUT MUST attempt to reattach by sending Parent Request using the parameters from Active Operational + # Dataset (Channel = ‘Primary’, PANID: 0xFACE). + # - The DUT MUST then attach using the parameters from the Pending Operational Dataset (Channel = ‘Secondary’, + # PANID: 0xAFCE). + print("Step 9: DUTs restart and reattach.") + # Search for reattach attempts for each DUT. + for dut in DUT_EXTADDRS: + # Per spec, DUT MUST attempt to reattach on old params first. + # We start searching from the same point for each DUT to allow intermingled packets. + pkts_dut = pkts.copy() + pkts_dut.filter_wpan_src64(dut). \ + filter(lambda p: p.wpan_tap.ch_num == 11 and p.wpan.dst_pan == 0xFACE). \ + filter(lambda p: p.mle.cmd == consts.MLE_PARENT_REQUEST). \ + must_next() + + # Then, it MUST attach using the new params. + pkts_dut.filter_wpan_src64(dut). \ + filter(lambda p: p.wpan_tap.ch_num == 12 and p.wpan.dst_pan == 0xAFCE). \ + filter(lambda p: p.mle.cmd == consts.MLE_PARENT_REQUEST). \ + must_next() + + # Step 10: Commissioner + # - Description: Harness verifies connectivity by instructing the Commissioner to send an ICMPv6 Echo Request to + # the DUT mesh local address. + # - Pass Criteria: The DUT MUST respond with an ICMPv6 Echo Reply. + print("Step 10: Verify connectivity via ICMPv6 Echo.") + for name, dut in zip(DUT_NAMES, DUT_EXTADDRS): + pkts.filter_wpan_src64(COMMISSIONER). \ + filter(lambda p: p.icmpv6.type == consts.ICMPV6_TYPE_ECHO_REQUEST). \ + must_next() + + pkts.filter_wpan_src64(dut). \ + filter(lambda p: p.icmpv6.type == consts.ICMPV6_TYPE_ECHO_REPLY). \ + must_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify) diff --git a/tests/nexus/verify_utils.py b/tests/nexus/verify_utils.py index baa3c030e..7b5b94528 100644 --- a/tests/nexus/verify_utils.py +++ b/tests/nexus/verify_utils.py @@ -75,6 +75,10 @@ def thread_coap_tlv_parse(t, v, layer=None): kvs.append(('channel', struct.unpack('>H', v[1:3])[0])) elif t == consts.NM_ACTIVE_TIMESTAMP_TLV and len(v) == 8 and not is_diag: kvs.append(('active_timestamp', struct.unpack('>Q', v)[0] >> 16)) + elif t == consts.NM_PENDING_TIMESTAMP_TLV and len(v) == 8 and not is_diag: + kvs.append(('pending_timestamp', struct.unpack('>Q', v)[0] >> 16)) + elif t == consts.NM_DELAY_TIMER_TLV and len(v) == 4 and not is_diag: + kvs.append(('delay_timer', struct.unpack('>I', v)[0])) elif t == consts.NM_CHANNEL_MASK_TLV and not is_diag: kvs.append(('channel_mask', v.hex())) elif t == consts.NM_EXTENDED_PAN_ID_TLV and len(v) == 8 and not is_diag: @@ -150,7 +154,32 @@ def thread_coap_tlv_parse(t, v, layer=None): def apply_patches(): CoapTlvParser.parse = staticmethod(thread_coap_tlv_parse) - from pktverify import layer_fields + from pktverify import consts, layer_fields + consts.VALID_LAYER_NAMES.add('wpan_tap') + consts.VALID_LAYER_NAMES.add('wpan-tap') + + # Patch _get_candidate_layers to map wpan_tap to wpan-tap + old_get_candidate_layers = layer_fields._get_candidate_layers + + def patched_get_candidate_layers(packet, layer_name): + if layer_name == 'wpan_tap': + layer_name = 'wpan-tap' + return old_get_candidate_layers(packet, layer_name) + + layer_fields._get_candidate_layers = patched_get_candidate_layers + + # Patch Layer.get_field to handle wpan_tap.ch_num + from pyshark.packet.layer import Layer + old_get_field = Layer.get_field + + def patched_get_field(self, name): + v = old_get_field(self, name) + if v is None and name == 'wpan_tap.ch_num': + v = old_get_field(self, 'wpan-tap.ch_num') + return v + + Layer.get_field = patched_get_field + layer_fields._LAYER_FIELDS['coap.tlv.tlv_request'] = layer_fields._bytes layer_fields._LAYER_FIELDS['mle.tlv.active_operational_dataset'] = layer_fields._bytes layer_fields._LAYER_FIELDS['mle.tlv.pending_operational_dataset'] = layer_fields._bytes @@ -168,7 +197,11 @@ def apply_patches(): layer_fields._LAYER_FIELDS['coap.tlv.border_agent_rloc16'] = layer_fields._auto layer_fields._LAYER_FIELDS['coap.tlv.channel'] = layer_fields._auto layer_fields._LAYER_FIELDS['coap.tlv.active_timestamp'] = layer_fields._auto + layer_fields._LAYER_FIELDS['coap.tlv.pending_timestamp'] = layer_fields._auto + layer_fields._LAYER_FIELDS['coap.tlv.delay_timer'] = layer_fields._auto layer_fields._LAYER_FIELDS['coap.tlv.channel_mask'] = layer_fields._bytes + layer_fields._LAYER_FIELDS['wpan_tap.ch_num'] = layer_fields._auto + layer_fields._LAYER_FIELDS['wpan-tap.ch_num'] = layer_fields._auto layer_fields._LAYER_FIELDS['coap.tlv.ext_pan_id'] = layer_fields._bytes layer_fields._LAYER_FIELDS['coap.tlv.network_name'] = layer_fields._str layer_fields._LAYER_FIELDS['coap.tlv.pskc'] = layer_fields._bytes