[nexus] add test 9.2.17 Orphaned End Devices (#12548) (#12562)

This commit adds Nexus test case 9.2.17 which verifies the behavior of
an orphaned end device as it searches for a new parent using MLE
Announce messages.

Implementation details:
- tests/nexus/test_9_2_17.cpp: C++ test execution logic. Implements the
  3-node topology (Leader_1, Leader_2, ED_1) using direct method calls.
  Manages RF isolation by manipulating the Mac Filter AllowList. Sets
  log level to note. Uses MED mode for the DUT to enable MLE Announce
  behavior. Also explicitly sets the channel mask for Leader_2 for
  robustness.
- tests/nexus/verify_9_2_17.py: PCAP verification script. Verifies
  initial MLE Advertisements on separate channels, DUT sending MLE
  Parent Requests on the primary channel, followed by MLE Announce
  on the secondary channel. Verifies Leader_2 sending MLE Announce on
  the primary channel and DUT successfully attaching to Leader_2.
- tests/nexus/run_nexus_tests.sh: Added 9_2_17 to default test list.
- tests/nexus/CMakeLists.txt: Added nexus_9_2_17 target.
- Included full test specification as inline comments in both C++ and
  Python code.
This commit is contained in:
Jonathan Hui
2026-02-25 21:41:18 -06:00
committed by GitHub
parent 8612d40adf
commit 3758ac4b48
4 changed files with 516 additions and 0 deletions
+1
View File
@@ -209,6 +209,7 @@ ot_nexus_test(9_2_13 "cert;nexus")
ot_nexus_test(9_2_14 "cert;nexus")
ot_nexus_test(9_2_15 "cert;nexus")
ot_nexus_test(9_2_16 "cert;nexus")
ot_nexus_test(9_2_17 "cert;nexus")
# Misc tests
ot_nexus_test(border_admitter "core;nexus")
+1
View File
@@ -145,6 +145,7 @@ DEFAULT_TESTS=(
"9_2_14"
"9_2_15"
"9_2_16"
"9_2_17"
)
# Use provided arguments or the default test list
+343
View File
@@ -0,0 +1,343 @@
/*
* 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 <stdio.h>
#include <string.h>
#include "meshcop/dataset.hpp"
#include "platform/nexus_core.hpp"
#include "platform/nexus_node.hpp"
#include "thread/mle.hpp"
#include "thread/thread_netif.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 = 10 * 1000;
/**
* Time to advance for the DUT to recognize that its parent is gone, in milliseconds.
*/
static constexpr uint32_t kParentLossTime = 60 * 1000;
/**
* Time to advance for the DUT to send Parent Request and receive no response, in milliseconds.
*/
static constexpr uint32_t kParentSelectionTime = 10 * 1000;
/**
* Time to advance for the DUT to send MLE Announce and receive MLE Announce response, in milliseconds.
*/
static constexpr uint32_t kAnnounceTime = 10 * 1000;
/**
* Time to advance for the DUT to attach to a new parent, in milliseconds.
*/
static constexpr uint32_t kAttachTime = 20 * 1000;
/**
* Time to wait for ICMPv6 Echo response, in milliseconds.
*/
static constexpr uint32_t kEchoTimeout = 5000;
/**
* Primary Channel.
*/
static constexpr uint16_t kPrimaryChannel = 11;
/**
* Secondary Channel.
*/
static constexpr uint16_t kSecondaryChannel = 12;
/**
* Leader_1 PAN ID.
*/
static constexpr uint16_t kLeader1PanId = 0x1111;
/**
* Leader_2 PAN ID.
*/
static constexpr uint16_t kLeader2PanId = 0x2222;
/**
* Leader_1 Active Timestamp.
*/
static constexpr uint64_t kLeader1Timestamp = 10;
/**
* Leader_2 Active Timestamp.
*/
static constexpr uint64_t kLeader2Timestamp = 20;
/**
* Network Key.
*/
static const uint8_t kNetworkKey[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff};
void Test9_2_17(void)
{
/**
* 9.2.17 Orphaned End Devices
*
* 9.2.17.1 Topology
* - Leader_1
* - Leader_2
* - ED_1 (DUT)
*
* 9.2.17.2 Purpose & Description
* The purpose of this test case to validate end device functionality when its Parent is no longer available and
* searches for a new Parent using MLE Announce messages.
*
* Spec Reference | V1.1 Section | V1.3.0 Section
* ---------------------|--------------|---------------
* Orphaned End Devices | 8.7.7 | 8.7.7
*/
Core nexus;
Node &leader1 = nexus.CreateNode();
Node &leader2 = nexus.CreateNode();
Node &dut = nexus.CreateNode();
leader1.SetName("LEADER_1");
leader2.SetName("LEADER_2");
dut.SetName("DUT");
nexus.AdvanceTime(0);
Instance::SetLogLevel(kLogLevelNote);
Log("---------------------------------------------------------------------------------------");
Log("Step 1: All");
/**
* Step 1: All
* - Description: Form the two topologies and ensure the DUT is attached to Leader_1
* - Pass Criteria: Ensure topology is formed correctly. Verify that Leader_1 & Leader_2 are sending MLE
* Advertisements on separate channels.
*/
leader1.AllowList(dut);
dut.AllowList(leader1);
leader2.AllowList(dut);
dut.AllowList(leader2);
// Leader 1 <-> DUT is allowed.
// Leader 2 <-> DUT is initially blocked by radio channel and different PAN ID, but here we also use AllowList.
// We start with Leader 2 being reachable but on different channel.
// Actually, to fully control connectivity as per spec, we'll unallow Leader 2 for now.
leader2.UnallowList(dut);
dut.UnallowList(leader2);
dut.Get<Mle::Mle>().SetTimeout(20);
{
MeshCoP::Dataset::Info datasetInfo;
SuccessOrQuit(datasetInfo.GenerateRandom(leader1.GetInstance()));
datasetInfo.Set<MeshCoP::Dataset::kChannel>(kPrimaryChannel);
datasetInfo.Set<MeshCoP::Dataset::kPanId>(kLeader1PanId);
{
NetworkKey &networkKey = datasetInfo.Update<MeshCoP::Dataset::kNetworkKey>();
memcpy(networkKey.m8, kNetworkKey, sizeof(networkKey));
}
{
MeshCoP::Timestamp timestamp;
timestamp.SetSeconds(kLeader1Timestamp);
timestamp.SetTicks(0);
datasetInfo.Set<MeshCoP::Dataset::kActiveTimestamp>(timestamp);
}
datasetInfo.Set<MeshCoP::Dataset::kChannelMask>((1 << kPrimaryChannel) | (1 << kSecondaryChannel));
leader1.Get<MeshCoP::ActiveDatasetManager>().SaveLocal(datasetInfo);
leader1.Get<ThreadNetif>().Up();
SuccessOrQuit(leader1.Get<Mle::Mle>().Start());
}
{
MeshCoP::Dataset::Info datasetInfo;
SuccessOrQuit(datasetInfo.GenerateRandom(leader2.GetInstance()));
datasetInfo.Set<MeshCoP::Dataset::kChannel>(kSecondaryChannel);
datasetInfo.Set<MeshCoP::Dataset::kPanId>(kLeader2PanId);
{
NetworkKey &networkKey = datasetInfo.Update<MeshCoP::Dataset::kNetworkKey>();
memcpy(networkKey.m8, kNetworkKey, sizeof(networkKey));
}
{
MeshCoP::Timestamp timestamp;
timestamp.SetSeconds(kLeader2Timestamp);
timestamp.SetTicks(0);
datasetInfo.Set<MeshCoP::Dataset::kActiveTimestamp>(timestamp);
}
datasetInfo.Set<MeshCoP::Dataset::kChannelMask>((1 << kPrimaryChannel) | (1 << kSecondaryChannel));
leader2.Get<MeshCoP::ActiveDatasetManager>().SaveLocal(datasetInfo);
leader2.Get<ThreadNetif>().Up();
SuccessOrQuit(leader2.Get<Mle::Mle>().Start());
}
nexus.AdvanceTime(kFormNetworkTime);
VerifyOrQuit(leader1.Get<Mle::Mle>().IsLeader());
VerifyOrQuit(leader2.Get<Mle::Mle>().IsLeader());
dut.Join(leader1, Node::kAsMed);
nexus.AdvanceTime(kJoinTime);
VerifyOrQuit(dut.Get<Mle::Mle>().IsAttached());
VerifyOrQuit(dut.Get<Mle::Mle>().GetParent().GetExtAddress() == leader1.Get<Mac::Mac>().GetExtAddress());
Log("---------------------------------------------------------------------------------------");
Log("Step 2: Leader_1");
/**
* Step 2: Leader_1
* - Description: Harness silently powers-down Leader_1 and enables connectivity between the DUT and Leader_2
* - Pass Criteria: N/A
*/
leader1.Get<Mle::Mle>().Stop();
leader1.Get<ThreadNetif>().Down();
leader2.AllowList(dut);
dut.AllowList(leader2);
Log("---------------------------------------------------------------------------------------");
Log("Step 3: DUT");
/**
* Step 3: DUT
* - Description: Automatically recognizes that its Parent is gone when it doesnt receive responses to MLE Child
* Update Requests
* - Pass Criteria: N/A
*/
nexus.AdvanceTime(kParentLossTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 4: DUT");
/**
* Step 4: DUT
* - Description: Automatically attempts to reattach to its current Thread Partition using the standard attaching
* process
* - Pass Criteria: The DUT MUST send a MLE Parent Request
*/
Log("---------------------------------------------------------------------------------------");
Log("Step 5: DUT");
/**
* Step 5: DUT
* - Description: The DUT does not receive a MLE Parent Response to its request
* - Pass Criteria: N/A
*/
nexus.AdvanceTime(kParentSelectionTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 6: DUT");
/**
* Step 6: DUT
* - Description: After failing to receive a MLE Parent Response to its request, the DUT automatically sends a MLE
* Announce Message on the Secondary channel and waits on the Primary channel to hear any announcements.
* - Pass Criteria: The DUT MUST send a MLE Announce Message, including the following TLVs:
* - Channel TLV: Primary
* - Active Timestamp TLV
* - PAN ID TLV
* - The Destination PAN ID in the IEEE 802.15.4 MAC header MUST be set to the Broadcast PAN ID (0xFFFF) and MUST
* be secured using Key ID Mode 2.
*/
nexus.AdvanceTime(kAnnounceTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 7: Leader_2");
/**
* Step 7: Leader_2
* - Description: Receives the MLE Announce from the DUT and automatically sends a MLE Announce on the Primary
* channel because Leader_2 has a new Active Timestamp
* - Pass Criteria: N/A
*/
nexus.AdvanceTime(kAnnounceTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 8: DUT");
/**
* Step 8: DUT
* - Description: Receives the MLE Announce from Leader_2 and automatically attempts to attach on the Secondary
* channel
* - Pass Criteria: The DUT MUST attempt to attach on the Secondary channel, with the new PAN ID it received in the
* MLE Announce message from Leader_2. The DUT MUST send a Parent Request on the Secondary channel
*/
nexus.AdvanceTime(kAttachTime);
VerifyOrQuit(dut.Get<Mle::Mle>().IsAttached());
VerifyOrQuit(dut.Get<Mle::Mle>().GetParent().GetExtAddress() == leader2.Get<Mac::Mac>().GetExtAddress());
Log("---------------------------------------------------------------------------------------");
Log("Step 9: Leader_2");
/**
* Step 9: Leader_2
* - Description: Harness verifies connectivity by instructing Leader_2 to send an ICMP Echo Request to the DUT
* - Pass Criteria: The DUT MUST respond with an ICMPv6 Echo Reply
*/
nexus.SendAndVerifyEchoRequest(leader2, dut.Get<Mle::Mle>().GetMeshLocalEid(), 0, 64, kEchoTimeout);
nexus.SaveTestInfo("test_9_2_17.json");
}
} // namespace Nexus
} // namespace ot
int main(void)
{
ot::Nexus::Test9_2_17();
printf("All tests passed\n");
return 0;
}
+171
View File
@@ -0,0 +1,171 @@
#!/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
def verify(pv):
# 9.2.17 Orphaned End Devices
#
# 9.2.17.1 Topology
# - Leader_1
# - Leader_2
# - ED_1 (DUT)
#
# 9.2.17.2 Purpose & Description
# The purpose of this test case to validate end device functionality when its Parent is no longer available and
# searches for a new Parent using MLE Announce messages.
#
# Spec Reference | V1.1 Section | V1.3.0 Section
# ---------------------|--------------|---------------
# Orphaned End Devices | 8.7.7 | 8.7.7
pkts = pv.pkts
pv.summary.show()
LEADER_1 = pv.vars['LEADER_1']
LEADER_2 = pv.vars['LEADER_2']
DUT = pv.vars['DUT']
PRIMARY_CHANNEL = 11
SECONDARY_CHANNEL = 12
LEADER_2_PAN_ID = 0x2222
BROADCAST_PAN_ID = 0xffff
# Step 1: All
# - Description: Form the two topologies and ensure the DUT is attached to Leader_1
# - Pass Criteria: Ensure topology is formed correctly. Verify that Leader_1 & Leader_2 are sending MLE
# Advertisements on separate channels.
print("Step 1: Verify that Leader_1 & Leader_2 are sending MLE Advertisements on separate channels.")
pkts.filter_wpan_src64(LEADER_1).\
filter_LLANMA().\
filter_mle_cmd(consts.MLE_ADVERTISEMENT).\
filter(lambda p: p.wpan_tap.ch_num == PRIMARY_CHANNEL).\
must_next()
pkts.filter_wpan_src64(LEADER_2).\
filter_LLANMA().\
filter_mle_cmd(consts.MLE_ADVERTISEMENT).\
filter(lambda p: p.wpan_tap.ch_num == SECONDARY_CHANNEL).\
must_next()
# Step 2: Leader_1
# - Description: Harness silently powers-down Leader_1 and enables connectivity between the DUT and Leader_2
# - Pass Criteria: N/A
print("Step 2: Harness silently powers-down Leader_1 and enables connectivity between the DUT and Leader_2")
# Step 3: DUT
# - Description: Automatically recognizes that its Parent is gone when it doesnt receive responses to MLE Child
# Update Requests
# - Pass Criteria: N/A
print("Step 3: DUT automatically recognizes that its Parent is gone")
# Step 4: DUT
# - Description: Automatically attempts to reattach to its current Thread Partition using the standard attaching
# process
# - Pass Criteria: The DUT MUST send a MLE Parent Request
print("Step 4: The DUT MUST send a MLE Parent Request")
pkts.filter_wpan_src64(DUT).\
filter_mle_cmd(consts.MLE_PARENT_REQUEST).\
filter(lambda p: p.wpan_tap.ch_num == PRIMARY_CHANNEL).\
must_next()
# Step 5: DUT
# - Description: The DUT does not receive a MLE Parent Response to its request
# - Pass Criteria: N/A
print("Step 5: The DUT does not receive a MLE Parent Response to its request")
# Step 6: DUT
# - Description: After failing to receive a MLE Parent Response to its request, the DUT automatically sends a MLE
# Announce Message on the Secondary channel and waits on the Primary channel to hear any announcements.
# - Pass Criteria: The DUT MUST send a MLE Announce Message, including the following TLVs:
# - Channel TLV: Primary
# - Active Timestamp TLV
# - PAN ID TLV
# - The Destination PAN ID in the IEEE 802.15.4 MAC header MUST be set to the Broadcast PAN ID (0xFFFF) and MUST
# be secured using Key ID Mode 2.
print("Step 6: The DUT MUST send a MLE Announce Message")
pkts.filter_wpan_src64(DUT).\
filter_mle_cmd(consts.MLE_ANNOUNCE).\
filter(lambda p: p.wpan_tap.ch_num == SECONDARY_CHANNEL).\
filter(lambda p: {
consts.CHANNEL_TLV,
consts.ACTIVE_TIMESTAMP_TLV,
consts.PAN_ID_TLV
} <= set(p.mle.tlv.type)).\
filter(lambda p: p.mle.tlv.channel == PRIMARY_CHANNEL).\
filter(lambda p: p.wpan.dst_pan == BROADCAST_PAN_ID).\
filter(lambda p: p.wpan.aux_sec.key_id_mode == 0x2).\
must_next()
# Step 7: Leader_2
# - Description: Receives the MLE Announce from the DUT and automatically sends a MLE Announce on the Primary
# channel because Leader_2 has a new Active Timestamp
# - Pass Criteria: N/A
print("Step 7: Leader_2 sends a MLE Announce on the Primary channel")
pkts.filter_wpan_src64(LEADER_2).\
filter_mle_cmd(consts.MLE_ANNOUNCE).\
filter(lambda p: p.wpan_tap.ch_num == PRIMARY_CHANNEL).\
must_next()
# Step 8: DUT
# - Description: Receives the MLE Announce from Leader_2 and automatically attempts to attach on the Secondary
# channel
# - Pass Criteria: The DUT MUST attempt to attach on the Secondary channel, with the new PAN ID it received in the
# MLE Announce message from Leader_2. The DUT MUST send a Parent Request on the Secondary channel
print("Step 8: The DUT MUST send a Parent Request on the Secondary channel")
pkts.filter_wpan_src64(DUT).\
filter_mle_cmd(consts.MLE_PARENT_REQUEST).\
filter(lambda p: p.wpan_tap.ch_num == SECONDARY_CHANNEL).\
filter(lambda p: p.wpan.dst_pan == LEADER_2_PAN_ID).\
must_next()
# Step 9: Leader_2
# - Description: Harness verifies connectivity by instructing Leader_2 to send an ICMP Echo Request to the DUT
# - Pass Criteria: The DUT MUST respond with an ICMPv6 Echo Reply
print("Step 9: ICMPv6 Echo Request/Reply")
_pkt = pkts.filter_ping_request().\
filter_ipv6_src(pv.vars['LEADER_2_MLEID']).\
filter_ipv6_dst(pv.vars['DUT_MLEID']).\
must_next()
pkts.filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).\
filter_ipv6_src(pv.vars['DUT_MLEID']).\
filter_ipv6_dst(pv.vars['LEADER_2_MLEID']).\
must_next()
if __name__ == '__main__':
verify_utils.run_main(verify)