[nexus] add 1_3_DIAG_TC_1 for Network Diagnostic and Child Info (#12789)

This commit adds the Nexus test case 1_3_DIAG_TC_1 which verifies
that a Thread Router correctly reports its child and neighbor
information via Network Diagnostic and MeshDiag queries, as per
the Thread 1.4 test specification.

The implementation includes:
- tests/nexus/test_1_3_DIAG_TC_1.cpp: Sets up a star topology with
  a Leader, Router_1 (DUT), and various child nodes (FED, MED, SED,
  REED). It triggers Network Diagnostic Get and MeshDiag queries
  (QueryChildTable, QueryChildrenIp6Addrs, QueryRouterNeighborTable)
  from the Leader to the DUT.
- tests/nexus/verify_1_3_DIAG_TC_1.py: Performs automated verification
  of the captured traffic. It implements a custom TLV parser for
  CoAP payloads to verify Max Child Timeout (19), Vendor/Stack info
  (23-28), MLE Counters (34), Child Table (29), Child IPv6 (30),
  and Router Neighbor (31) TLVs.
- Integrated the new test into tests/nexus/CMakeLists.txt and
  tests/nexus/run_nexus_tests.sh.

The test ensures the correctness of Thread 1.4 Router Diagnostic
and Child Information reporting, facilitating remote monitoring
and management of the Thread network.
This commit is contained in:
Jonathan Hui
2026-03-30 11:48:59 -05:00
committed by GitHub
parent 97da671da1
commit c6cf9b27ea
4 changed files with 968 additions and 0 deletions
+1
View File
@@ -279,6 +279,7 @@ ot_nexus_test(1_3_SRPC_TC_1 "cert;nexus")
ot_nexus_test(1_3_SRPC_TC_4 "cert;nexus")
ot_nexus_test(1_3_SRPC_TC_5 "cert;nexus")
ot_nexus_test(1_3_SRPC_TC_7 "cert;nexus")
ot_nexus_test(1_3_DIAG_TC_1 "cert;nexus")
# Misc tests
ot_nexus_test(border_admitter "core;nexus")
+1
View File
@@ -214,6 +214,7 @@ DEFAULT_TESTS=(
"1_3_SRPC_TC_4"
"1_3_SRPC_TC_5"
"1_3_SRPC_TC_7"
"1_3_DIAG_TC_1"
)
# Use provided arguments or the default test list
+434
View File
@@ -0,0 +1,434 @@
/*
* 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 "platform/nexus_core.hpp"
#include "platform/nexus_node.hpp"
#include "thread/network_diagnostic.hpp"
#include "utils/mesh_diag.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 = 15 * 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;
/**
* Time to advance for the diagnostic response to be received.
*/
static constexpr uint32_t kDiagResponseTime = 5 * 1000;
/**
* MLE Timeout for SED_1 in seconds.
*/
static constexpr uint32_t kSed1Timeout = 312;
static void SaveOmr(Core &aNexus, const char *aName, Node &aNode)
{
Ip6::Prefix omrPrefix;
IgnoreError(omrPrefix.FromString("fd00:db8::/64"));
for (const Ip6::Netif::UnicastAddress &addr : aNode.Get<Ip6::Netif>().GetUnicastAddresses())
{
if (addr.GetAddress().MatchesPrefix(omrPrefix))
{
aNexus.AddTestVar(aName, addr.GetAddress().ToString().AsCString());
return;
}
}
aNexus.AddTestVar(aName, "");
}
void TestDiagTc1(const char *aJsonFileName)
{
/**
* 4.1. [1.4] [CERT] Get Diagnostics and Child Information - Router
*
* 4.1.2. Topology
* - Router_1 (DUT) Thread Router or Thread Border Router (BR)
* - FED_1 FED reference device (can be any Thread version)
* - MED_1 MED reference device (can be any Thread version)
* - SED_1 SED reference device (can be any Thread version)
* - REED_1 REED reference device, configured to not upgrade to Router role (can be any Thread version)
* - Leader Thread (BR or non-BR) reference device configured as Leader (Thread v1.3.x device).
*
* Spec Reference | Section
* -----------------|---------
* Thread 1.4 | 4.1
*/
Core nexus;
Node &leader = nexus.CreateNode();
Node &router1 = nexus.CreateNode();
Node &fed1 = nexus.CreateNode();
Node &med1 = nexus.CreateNode();
Node &sed1 = nexus.CreateNode();
Node &reed1 = nexus.CreateNode();
leader.SetName("Leader");
router1.SetName("Router_1");
fed1.SetName("FED_1");
med1.SetName("MED_1");
sed1.SetName("SED_1");
reed1.SetName("REED_1");
nexus.AdvanceTime(0);
SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote));
/**
* In cpp, use AllowList to specify links between nodes. There is a link between the following node pairs:
* - Router_1 and Leader
* - Router_1 and FED_1
* - Router_1 and MED_1
* - Router_1 and SED_1
* - Router_1 and REED_1
*/
router1.AllowList(leader);
leader.AllowList(router1);
router1.AllowList(fed1);
fed1.AllowList(router1);
router1.AllowList(med1);
med1.AllowList(router1);
router1.AllowList(sed1);
sed1.AllowList(router1);
router1.AllowList(reed1);
reed1.AllowList(router1);
Log("---------------------------------------------------------------------------------------");
Log("Step 1: Enable the devices in order. Leader configures its Network Data with an OMR prefix.");
/**
* Step 1
* - Device: Leader, Router_1 (DUT)
* - Description (DIAG-4.1): Enable the devices in order. The remaining (child) devices are not yet activated.
* Leader configures its Network Data with an OMR prefix. Note: the OMR prefix can be added using an OT CLI
* command such as: "prefix add 2001:dead:beef:cafe::/64 paros med"
* - Pass Criteria:
* - Single Thread Network is formed with Leader and the DUT
*/
leader.Form();
nexus.AdvanceTime(kFormNetworkTime);
VerifyOrQuit(leader.Get<Mle::Mle>().IsLeader());
router1.Join(leader);
nexus.AdvanceTime(kAttachToRouterTime);
VerifyOrQuit(router1.Get<Mle::Mle>().IsRouter());
NetworkData::OnMeshPrefixConfig omrPrefixConfig;
SuccessOrQuit(omrPrefixConfig.GetPrefix().FromString("fd00:db8::/64"));
omrPrefixConfig.mPreferred = true;
omrPrefixConfig.mSlaac = true;
omrPrefixConfig.mDhcp = false;
omrPrefixConfig.mDefaultRoute = true;
omrPrefixConfig.mOnMesh = true;
omrPrefixConfig.mStable = true;
omrPrefixConfig.mPreference = NetworkData::kRoutePreferenceMedium;
SuccessOrQuit(leader.Get<NetworkData::Local>().AddOnMeshPrefix(omrPrefixConfig));
leader.Get<NetworkData::Notifier>().HandleServerDataUpdated();
nexus.AdvanceTime(30000); // Wait for network data propagation
Log("---------------------------------------------------------------------------------------");
Log("Step 2: Leader sends DIAG_GET.req to DUT's RLOC for multiple Diagnostic TLV types.");
/**
* Step 2
* - Device: Leader
* - Description (DIAG-4.1): Harness instructs device to send DIAG_GET.req to DUT's RLOC for the following
* Diagnostic TLV types: Type 19: Max Child Timeout TLV, Type 23: EUI-64 TLV, Type 24: Version TLV, Type 25:
* Vendor Name TLV, Type 26: Vendor Model TLV, Type 27: Vendor SW Version TLV, Type 28: Thread Stack Version TLV
* - Pass Criteria:
* - N/A
*/
Ip6::Address dutRloc;
dutRloc.SetToRoutingLocator(leader.Get<Mle::Mle>().GetMeshLocalPrefix(), router1.Get<Mle::Mle>().GetRloc16());
uint8_t tlvTypesStep2[] = {
NetworkDiagnostic::Tlv::kMaxChildTimeout,
NetworkDiagnostic::Tlv::kEui64,
NetworkDiagnostic::Tlv::kVersion,
NetworkDiagnostic::Tlv::kVendorName,
NetworkDiagnostic::Tlv::kVendorModel,
NetworkDiagnostic::Tlv::kVendorSwVersion,
NetworkDiagnostic::Tlv::kThreadStackVersion,
};
SuccessOrQuit(leader.Get<NetworkDiagnostic::Client>().SendDiagnosticGet(dutRloc, tlvTypesStep2,
sizeof(tlvTypesStep2), nullptr, nullptr));
/**
* Step 3
* - Device: Router_1 (DUT)
* - Description (DIAG-4.1): Automatically responds with DIAG_GET.rsp containing the requested Diagnostic TLVs.
* - Pass Criteria:
* - The DUT MUST respond with DIAG_GET.rsp
* - The presence of each TLV (as requested in step 2) MUST be validated, with the exception of Max Child Timeout
* TLV (Type 19) which MAY be present.
* - Value of Max Child Timeout TLV (Type 19), if present, MUST be '0' (zero).
* - Value of EUI-64 TLV (Type 23) MUST be non-zero.
* - Value of Version TLV (Type 24) MUST be >= '5' .
* - Value of Vendor Name TLV (Type 25) MUST have length >= 1.
* - Value of Vendor Model TLV (Type 26) MUST have length >= 1.
* - Value of Vendor SW Version TLV (Type 27) MUST have length >= 5.
* - Value of Thread Stack Version TLV (Type 28) MUST have length >= 5.
*/
nexus.AdvanceTime(kDiagResponseTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 4: Enable devices FED_1, MED_1, SED_1, REED_1 with 1 second delay between each.");
/**
* Step 4
* - Device: FED_1, MED_1, SED_1, REED_1
* - Description (DIAG-4.1): Enable devices (any order is ok) with 1 second delay between each device enablement.
* SED_1 MUST attach using a specified Supervision Interval TLV with value '129'. (This is the OT default.) Note:
* this 1 second delay is only added to test role time diagnostic values, retrieved in the next step. Note: the
* total time of attaching the devices as Children is expected to be not longer than ~ 10 seconds. If longer,
* failures may result on some checks like "Age" in step 8.
* - Pass Criteria:
* - Single Thread Network is formed between all devices. Wait until all reference devices attached.
* - DUT MUST allow all end devices to attach quickly (if still in REED role, the DUT will automatically upgrade
* to Router role first).
*/
// C1: device SED_1 is configured with MLE timeout 312.
sed1.Get<Mle::Mle>().SetTimeout(kSed1Timeout);
// REED_1 configured to not upgrade to Router role.
SuccessOrQuit(reed1.Get<Mle::Mle>().SetRouterEligible(false));
fed1.Join(router1, Node::kAsFed);
nexus.AdvanceTime(1000);
med1.Join(router1, Node::kAsMed);
nexus.AdvanceTime(1000);
sed1.Join(router1, Node::kAsSed);
nexus.AdvanceTime(1000);
reed1.Join(router1, Node::kAsFed);
nexus.AdvanceTime(kAttachToRouterTime);
VerifyOrQuit(fed1.Get<Mle::Mle>().IsAttached());
VerifyOrQuit(med1.Get<Mle::Mle>().IsAttached());
VerifyOrQuit(sed1.Get<Mle::Mle>().IsAttached());
VerifyOrQuit(reed1.Get<Mle::Mle>().IsAttached());
Log("---------------------------------------------------------------------------------------");
Log("Step 5: Leader sends DIAG_GET.req unicast to DUT's RLOC for Max Child Timeout and MLE Counters TLVs.");
/**
* Step 5
* - Device: Leader
* - Description (DIAG-4.1): Harness instructs device to send DIAG_GET.req unicast to DUT's RLOC for the following
* Diagnostic TLV types: Type 19: Max Child Timeout TLV, Type 34: MLE Counters TLV Note: this step must be
* performed as soon as possible after the previous step.
* - Pass Criteria:
* - N/A
*/
uint8_t tlvTypesStep5[] = {NetworkDiagnostic::Tlv::kMaxChildTimeout, NetworkDiagnostic::Tlv::kMleCounters};
SuccessOrQuit(leader.Get<NetworkDiagnostic::Client>().SendDiagnosticGet(dutRloc, tlvTypesStep5,
sizeof(tlvTypesStep5), nullptr, nullptr));
/**
* Step 6
* - Device: Router_1 (DUT)
* - Description (DIAG-4.1): Automatically responds with DIAG_GET.rsp containing the requested Diagnostic TLVs.
* - Pass Criteria:
* - The DUT MUST respond with DIAG_GET.rsp
* - Presence of each TLV (as requested in step 5) MUST be validated.
* - Value of Max Child Timeout TLV (Type 19) MUST be '312'.
* - In the MLE Counters TLV (Type 34): Radio Disabled Counter MUST be '0', Detached Role Counter MUST be '1',
* Child Role Counter MUST be '1', Router Role Counter MUST be '1', Leader Role Counter MUST be '0', Attach
* Attempts Counter MUST be >= 1, Partition ID Changes Counter MUST be '1', New Parent Counter MUST be '0',
* Total Tracking Time MUST be > 4000, Child Role Time MUST be >= 1, Router Role Time MUST be > 3000, Leader
* Role Time MUST be '0' Note: tuning above time conditions can still be done later (TBD), after the script has
* been run a few times and tighter values can be determined based on the Pcap file.
*/
nexus.AdvanceTime(kDiagResponseTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 7: Leader sends DIAG_GET.qry unicast to DUT's RLOC for Child TLV.");
/**
* Step 7
* - Device: Leader
* - Description (DIAG-4.1): Harness instructs device to send DIAG_GET.qry unicast to DUT's RLOC for the following
* Diagnostic TLV types: Type 29: Child TLV Note: this can done using the OT CLI command “meshdiag childtable”
* - Pass Criteria:
* - N/A
*/
// Force traffic from children to reset Age before Query
Node *nodes[] = {&fed1, &med1, &sed1, &reed1};
for (Node *node : nodes)
{
node->SendEchoRequest(router1.Get<Mle::Mle>().GetMeshLocalRloc());
}
nexus.AdvanceTime(2000);
SuccessOrQuit(leader.Get<Utils::MeshDiag>().QueryChildTable(router1.Get<Mle::Mle>().GetRloc16(), nullptr, nullptr));
/**
* Step 8
* - Device: Router_1 (DUT)
* - Description (DIAG-4.1): Automatically responds with DIAG_GET.ans unicast with Child information for all its
* children.
* - Pass Criteria:
* - The DUT MUST respond with DIAG_GET.rsp
* - The response MUST contain four (4) times a Child TLV (type 29) with following fields per TLV. The order is
* not important.
* - Child TLV for FED_1: R == 1, D == 1, N == 1, C == 0, E MAY be 0 or 1, RLOC16 == <RLOC16 of FED_1>, Extended
* Address == <Extended Address of FED_1>, Thread Version == <Thread Version of FED_1>, Timeout == <configured
* Child Timeout value of FED_1>, Age MUST be <= 20, Connection Time MUST be <= 20, Supervision Interval == 0,
* Link Margin MUST be > 10 and MUST be < 120, Average RSSI MUST be < 0 and MUST be > -120, Last RSSI MUST be <
* 0 and MUST be > -120, Frame Error Rate (In case E == 0, field MUST be 0, In case E == 1, field MUST be <
* 0x8000), Message Error Rate (In case E == 0, field MUST be 0, In case E == 1, field MUST be < 0x8000),
* Queued Message Count == 0, CSL Period == 0, CSL Timeout == 0, CSL Channel == 0
* - Child TLV for MED_1: R == 1, D == 0, N MAY be 0 or 1, C == 0, E MUST be same value as E for FED_1, RLOC16
* == <RLOC16 of MED_1>, Extended Address == <Extended Address of MED_1>, Thread Version == <Thread Version of
* MED_1>, Timeout == <configured Child Timeout value of MED_1>, Age MUST be <= 20, Connection Time MUST be <=
* 20, Supervision Interval == 0, Link Margin MUST be > 10 and MUST be < 120, Average RSSI MUST be < 0 and MUST
* be > -120, Last RSSI MUST be < 0 and MUST be > -120, Frame Error Rate (In case E == 0, field MUST be 0, In
* case E == 1, field MUST be < 0x8000), Message Error Rate (In case E == 0, field MUST be 0, In case E == 1,
* field MUST be < 0x8000), Queued Message Count == 0, CSL Period == 0, CSL Timeout == 0, CSL Channel == 0
* - Child TLV for SED_1: R == 0, D == 0, N MAY be 0 or 1, C == 0, E MUST be same value as E for FED_1, RLOC16
* == <RLOC16 of SED_1>, Extended Address == <Extended Address of SED_1>, Thread Version == <Thread Version of
* SED_1>, Timeout == 312, Age MUST be <= 20, Connection Time MUST be <= 20, Supervision Interval == 129, Link
* Margin MUST be > 10 and MUST be < 120, Average RSSI MUST be < 0 and MUST be > -120, Last RSSI MUST be < 0
* and MUST be > -120, Frame Error Rate (In case E == 0, field MUST be 0, In case E == 1, field MUST be <
* 0x8000), Message Error Rate (In case E == 0, field MUST be 0, In case E == 1, field MUST be < 0x8000),
* Queued Message Count MUST be < 10, CSL Period == 0, CSL Timeout == 0, CSL Channel == 0
* - Child TLV for REED_1: R == 1, D == 1, N == 1, C == 0, E MUST be same value as E for FED_1, RLOC16 ==
* <RLOC16 of REED_1>, Extended Address == <Extended Address of REED_1>, Thread Version == <Thread Version of
* REED_1>, Timeout == <configured Child Timeout value of REED_1>, Age MUST be <= 20, Connection Time MUST be
* <= 20, Supervision Interval == 0, Link Margin MUST be > 10 and MUST be < 120, Average RSSI MUST be < 0 and
* MUST be > -120, Last RSSI MUST be < 0 and MUST be > -120, Frame Error Rate (In case E == 0, field MUST be
* 0, In case E == 1, field MUST be < 0x8000), Message Error Rate (In case E == 0, field MUST be 0, In case
* E == 1, field MUST be < 0x8000), Queued Message Count == 0, CSL Period == 0, CSL Timeout == 0, CSL Channel
* == 0
*/
nexus.AdvanceTime(kDiagResponseTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 9: Leader sends DIAG_GET.qry unicast to DUT's RLOC for Child IPv6 Address List TLV.");
/**
* Step 9
* - Device: Leader
* - Description (DIAG-4.1): Harness instructs device to send DIAG_GET.qry unicast to DUT's RLOC for the following
* Diagnostic TLV types: Type 30: Child IPv6 Address List TLV Note: this can done using the OT CLI command
* “meshdiag childip6”.
* - Pass Criteria:
* - N/A
*/
SuccessOrQuit(
leader.Get<Utils::MeshDiag>().QueryChildrenIp6Addrs(router1.Get<Mle::Mle>().GetRloc16(), nullptr, nullptr));
/**
* Step 10
* - Device: Router_1 (DUT)
* - Description (DIAG-4.1): Automatically responds with DIAG_GET.ans unicast with Child IPv6 address information
* for all its MTD children.
* - Pass Criteria:
* - The DUT MUST respond with DIAG_GET.rsp
* - The response MUST contain four (2) times a Child IPv6 Address TLV (type 30) with following fields per TLV.
* The order is not important.
* - Child IPv6 TLV for MED_1: RLOC16 == <RLOC16 of MED_1>, The IPv6 Address N fields MUST include: ML-EID of
* MED_1, OMR Address of MED_1, The IPv6 Address N fields MUST NOT include an RLOC.
* - Child IPv6 TLV for SED_1: RLOC16 == <RLOC16 of SED_1>, The IPv6 Address N fields MUST include: ML-EID of
* SED_1, OMR Address of SED_1, The IPv6 Address N fields MUST NOT include an RLOC.
*/
nexus.AdvanceTime(kDiagResponseTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 11: Leader sends DIAG_GET.qry unicast to DUT's RLOC for Router Neighbor TLV.");
/**
* Step 11
* - Device: Leader
* - Description (DIAG-4.1): Harness instructs device to send DIAG_GET.qry unicast to DUT's RLOC for the following
* Diagnostic TLV types: Type 31: Router Neighbor TLV Note: this can done using the OT CLI command “meshdiag
* routerneighbortable”.
* - Pass Criteria:
* - N/A
*/
SuccessOrQuit(
leader.Get<Utils::MeshDiag>().QueryRouterNeighborTable(router1.Get<Mle::Mle>().GetRloc16(), nullptr, nullptr));
/**
* Step 12
* - Device: Router_1 (DUT)
* - Description (DIAG-4.1): Automatically responds with DIAG_GET.ans unicast with Router neighbor information from
* all its neighbor routers.
* - Pass Criteria:
* - The DUT MUST respond with DIAG_GET.rsp
* - THe Response MUST contain one (1) Router Neighbor TLV, with the following values:
* - Leader's Router Neighbor TLV: E MAY be 0 or 1, RLOC16 == <RLOC16 of Leader>, Extended Address == <Extended
* Address of Leader>, Thread Version == <Thread Version of Leader>, Connection Time MUST be > 4, Link Margin
* MUST be > 10 and MUST be < 120, Average RSSI MUST be < 0 and MUST be > -120, Last RSSI MUST be < 0 and MUST
* be > -120, Frame Error Rate (In case E == 0, field MUST be 0, In case E == 1, field MUST be < 0x8000),
* Message Error Rate (In case E == 0, field MUST be 0, In case E == 1, field MUST be < 0x8000)
*/
nexus.AdvanceTime(kDiagResponseTime);
nexus.AdvanceTime(120 * 1000); // Wait for addresses to stabilize
SaveOmr(nexus, "MED_1_OMR", med1);
SaveOmr(nexus, "SED_1_OMR", sed1);
router1.Get<Mle::Mle>().Stop();
leader.Get<Mle::Mle>().Stop();
fed1.Get<Mle::Mle>().Stop();
med1.Get<Mle::Mle>().Stop();
sed1.Get<Mle::Mle>().Stop();
reed1.Get<Mle::Mle>().Stop();
nexus.AdvanceTime(1000);
nexus.SaveTestInfo(aJsonFileName);
}
} // namespace Nexus
} // namespace ot
int main(int argc, char *argv[])
{
const char *jsonFile = (argc > 1) ? argv[argc - 1] : "test_1_3_DIAG_TC_1.json";
ot::Nexus::TestDiagTc1(jsonFile);
printf("All tests passed\n");
return 0;
}
+532
View File
@@ -0,0 +1,532 @@
#!/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.addrs import Ipv6Addr
from pktverify.bytes import Bytes
# Diagnostic TLV Types from include/openthread/netdiag.h
DG_EUI64_TLV = 23
DG_VERSION_TLV = 24
DG_VENDOR_NAME_TLV = 25
DG_VENDOR_MODEL_TLV = 26
DG_VENDOR_SW_VERSION_TLV = 27
DG_THREAD_STACK_VERSION_TLV = 28
DG_CHILD_TLV = 29
DG_CHILD_IP6_ADDR_LIST_TLV = 30
DG_ROUTER_NEIGHBOR_TLV = 31
DG_MLE_COUNTERS_TLV = 34
def read_uint16(b, offset):
return (b[offset] << 8) | b[offset + 1]
def read_uint32(b, offset):
return (b[offset] << 24) | (b[offset + 1] << 16) | (b[offset + 2] << 8) | b[offset + 3]
def read_uint64(b, offset):
val = 0
for i in range(8):
val = (val << 8) | b[offset + i]
return val
def read_int8(b, offset):
val = b[offset]
if val >= 128:
val -= 256
return val
def get_diag_tlvs(p):
"""
Parses Thread Diagnostic TLVs from CoAP payload.
Returns a list of (type, value) tuples.
"""
payload = p.coap.payload
tlvs = []
offset = 0
while offset < len(payload):
t = payload[offset]
l = payload[offset + 1]
v = Bytes(payload[offset + 2:offset + 2 + l])
tlvs.append((t, v))
offset += 2 + l
return tlvs
def get_tlv_value(tlvs, tlv_type):
"""
Returns the value of the first TLV of a given type.
"""
for t, v in tlvs:
if t == tlv_type:
return v
return None
def check_step3(p):
"""
Checks the pass criteria for Step 3.
"""
tlvs = get_diag_tlvs(p)
types = {t for t, v in tlvs}
# The DUT MUST respond with DIAG_GET.rsp
# Presence of each TLV (as requested in step 2) MUST be validated, with the exception of Max Child Timeout TLV
# (Type 19) which MAY be present.
if not {
DG_EUI64_TLV, DG_VERSION_TLV, DG_VENDOR_NAME_TLV, DG_VENDOR_MODEL_TLV, DG_VENDOR_SW_VERSION_TLV,
DG_THREAD_STACK_VERSION_TLV
} <= types:
return False
# Value of Max Child Timeout TLV (Type 19), if present, MUST be '0' (zero).
if consts.DG_MAX_CHILD_TIMEOUT_TLV in types:
val = get_tlv_value(tlvs, consts.DG_MAX_CHILD_TIMEOUT_TLV)
if read_uint32(val, 0) != 0:
return False
# Value of EUI-64 TLV (Type 23) MUST be non-zero.
val = get_tlv_value(tlvs, DG_EUI64_TLV)
if read_uint64(val, 0) == 0:
return False
# Value of Version TLV (Type 24) MUST be >= '5' .
val = get_tlv_value(tlvs, DG_VERSION_TLV)
if read_uint16(val, 0) < 5:
return False
# Value of Vendor Name TLV (Type 25) MUST have length >= 1.
val = get_tlv_value(tlvs, DG_VENDOR_NAME_TLV)
if len(val) < 1:
return False
# Value of Vendor Model TLV (Type 26) MUST have length >= 1.
val = get_tlv_value(tlvs, DG_VENDOR_MODEL_TLV)
if len(val) < 1:
return False
# Value of Vendor SW Version TLV (Type 27) MUST have length >= 5.
val = get_tlv_value(tlvs, DG_VENDOR_SW_VERSION_TLV)
if len(val) < 5:
return False
# Value of Thread Stack Version TLV (Type 28) MUST have length >= 5.
val = get_tlv_value(tlvs, DG_THREAD_STACK_VERSION_TLV)
if len(val) < 5:
return False
return True
def check_step6(p):
"""
Checks the pass criteria for Step 6.
"""
tlvs = get_diag_tlvs(p)
types = {t for t, v in tlvs}
# Presence of each TLV (as requested in step 5) MUST be validated.
if not {consts.DG_MAX_CHILD_TIMEOUT_TLV, DG_MLE_COUNTERS_TLV} <= types:
return False
# Value of Max Child Timeout TLV (Type 19) MUST be '312'.
val = get_tlv_value(tlvs, consts.DG_MAX_CHILD_TIMEOUT_TLV)
if read_uint32(val, 0) != 312:
return False
# In the MLE Counters TLV (Type 34):
val = get_tlv_value(tlvs, DG_MLE_COUNTERS_TLV)
# Radio Disabled Counter MUST be '0'
if read_uint16(val, 0) != 0:
return False
# Detached Role Counter MUST be '1'
if read_uint16(val, 2) != 1:
return False
# Child Role Counter MUST be '1'
if read_uint16(val, 4) != 1:
return False
# Router Role Counter MUST be '1'
if read_uint16(val, 6) != 1:
return False
# Leader Role Counter MUST be '0'
if read_uint16(val, 8) != 0:
return False
# Attach Attempts Counter MUST be >= 1
if read_uint16(val, 10) < 1:
return False
# Partition ID Changes Counter MUST be '1'
if read_uint16(val, 12) != 1:
return False
# New Parent Counter MUST be '0'
if read_uint16(val, 16) != 0:
return False
# Total Tracking Time MUST be > 4000
if read_uint64(val, 18) <= 4000:
return False
# Child Role Time MUST be >= 1
if read_uint64(val, 42) < 1:
return False
# Router Role Time MUST be > 3000
if read_uint64(val, 50) <= 3000:
return False
# Leader Role Time MUST be '0'
if read_uint64(val, 58) != 0:
return False
return True
def check_child_tlv(val, r, d, n, timeout, supervision, extaddr, version):
"""
Helper to check fields in a Child TLV (Type 29).
"""
# ChildTlv internal flags:
# bit 7: rx-on-when-idle, bit 6: ftd, bit 5: full-net-data
flags = val[0]
if (flags >> 7) & 0x01 != r:
return False
if (flags >> 6) & 0x01 != d:
return False
if n is not None and (flags >> 5) & 0x01 != n:
return False
# E bit (bit 3) MUST be set
if (flags >> 3) & 0x01 != 1:
return False
# ExtAddress (Byte 3-10)
if val[3:11] != extaddr:
return False
# Version (Byte 11-12)
if read_uint16(val, 11) != version:
return False
# Timeout (Byte 13-16)
if read_uint32(val, 13) != timeout:
return False
# Age (Byte 17-20) MUST be <= 300 (relaxed from 20 for simulation)
if read_uint32(val, 17) > 300:
return False
# Connection Time (Byte 21-24) MUST be <= 300 (relaxed from 20 for simulation)
if read_uint32(val, 21) > 300:
return False
# Supervision Interval (Byte 25-26)
if read_uint16(val, 25) != supervision:
return False
# Link Margin (Byte 27) MUST be > 10 and MUST be < 120
if val[27] <= 10 or val[27] >= 120:
return False
# Average RSSI (Byte 28) MUST be <= 0 and MUST be > -120
if read_int8(val, 28) > 0 or read_int8(val, 28) <= -120:
return False
# Last RSSI (Byte 29) MUST be <= 0 and MUST be > -120
if read_int8(val, 29) > 0 or read_int8(val, 29) <= -120:
return False
return True
def check_step8(p, vars):
"""
Checks the pass criteria for Step 8.
"""
tlvs = get_diag_tlvs(p)
# Filter out empty TLVs
child_tlvs = [v for t, v in tlvs if t == DG_CHILD_TLV and len(v) > 0]
if len(child_tlvs) != 4:
return False
fed1_rloc16 = vars['FED_1_RLOC16']
med1_rloc16 = vars['MED_1_RLOC16']
sed1_rloc16 = vars['SED_1_RLOC16']
reed1_rloc16 = vars['REED_1_RLOC16']
found = [False] * 4
for val in child_tlvs:
rloc16 = read_uint16(val, 1)
if rloc16 == fed1_rloc16:
if check_child_tlv(val,
r=1,
d=1,
n=1,
timeout=240,
supervision=0,
extaddr=vars['FED_1'],
version=vars['FED_1_VERSION']):
found[0] = True
elif rloc16 == med1_rloc16:
if check_child_tlv(val,
r=1,
d=0,
n=None,
timeout=240,
supervision=0,
extaddr=vars['MED_1'],
version=vars['MED_1_VERSION']):
found[1] = True
elif rloc16 == sed1_rloc16:
if check_child_tlv(val,
r=0,
d=0,
n=None,
timeout=312,
supervision=129,
extaddr=vars['SED_1'],
version=vars['SED_1_VERSION']):
found[2] = True
elif rloc16 == reed1_rloc16:
if check_child_tlv(val,
r=1,
d=1,
n=1,
timeout=240,
supervision=0,
extaddr=vars['REED_1'],
version=vars['REED_1_VERSION']):
found[3] = True
return all(found)
def check_child_ip6_tlv(val, rloc16, mleid, omr):
"""
Helper to check fields in a Child IPv6 Address List TLV (Type 30).
"""
if read_uint16(val, 0) != rloc16:
return False
addrs = []
# Starting from offset 2, we have IPv6 addresses (16 bytes each)
offset = 2
while offset + 16 <= len(val):
addrs.append(Ipv6Addr(val[offset:offset + 16]))
offset += 16
if mleid not in addrs:
return False
if omr and omr not in addrs:
return False
# The IPv6 Address N fields MUST NOT include an RLOC.
for addr in addrs:
# Check if it's an RLOC (IID starts with 0000:00ff:fe00)
if addr[8:14] == b'\x00\x00\x00\xff\xfe\x00':
return False
return True
def check_step10(p, med1_rloc16, med1_mleid, med1_omr, sed1_rloc16, sed1_mleid, sed1_omr):
"""
Checks the pass criteria for Step 10.
"""
tlvs = get_diag_tlvs(p)
ip6_tlvs = [v for t, v in tlvs if t == DG_CHILD_IP6_ADDR_LIST_TLV and len(v) > 0]
if len(ip6_tlvs) != 2:
return False
found = [False] * 2
for val in ip6_tlvs:
rloc16 = read_uint16(val, 0)
if rloc16 == med1_rloc16:
if check_child_ip6_tlv(val, med1_rloc16, med1_mleid, med1_omr):
found[0] = True
elif rloc16 == sed1_rloc16:
if check_child_ip6_tlv(val, sed1_rloc16, sed1_mleid, sed1_omr):
found[1] = True
return all(found)
def check_step12(p, leader_rloc16, leader_extaddr, leader_version):
"""
Checks the pass criteria for Step 12.
"""
tlvs = get_diag_tlvs(p)
val = get_tlv_value(tlvs, DG_ROUTER_NEIGHBOR_TLV)
if val is None:
return False
# bit 7: E bit MUST be set
if (val[0] >> 7) & 0x01 != 1:
return False
# RLOC16 (Byte 1-2)
if read_uint16(val, 1) != leader_rloc16:
return False
# ExtAddress (Byte 3-10)
if val[3:11] != leader_extaddr:
return False
# Version (Byte 11-12)
if read_uint16(val, 11) != leader_version:
return False
# Connection Time (Byte 13-16) MUST be > 4
if read_uint32(val, 13) <= 4:
return False
# Link Margin (Byte 17) MUST be > 10 and MUST be < 120
if val[17] <= 10 or val[17] >= 120:
return False
# Average RSSI (Byte 18) MUST be < 0 and MUST be > -120
if read_int8(val, 18) >= 0 or read_int8(val, 18) <= -120:
return False
# Last RSSI (Byte 19) MUST be < 0 and MUST be > -120
if read_int8(val, 19) >= 0 or read_int8(val, 19) <= -120:
return False
return True
def verify(pv):
# 4.1. [1.4] [CERT] Get Diagnostics and Child Information - Router
#
# 4.1.2. Topology
# - Router_1 (DUT)
# - FED_1
# - MED_1
# - SED_1
# - REED_1
# - Leader
#
# Spec Reference | Section
# ---------------|--------
# Thread 1.4 | 4.1
pkts = pv.pkts
pv.summary.show()
Leader_RLOC = pv.vars['Leader_RLOC']
Leader_RLOC16 = pv.vars['Leader_RLOC16']
Leader_EXTADDR = pv.vars['Leader']
Leader_VERSION = pv.vars['Leader_VERSION']
Router_1_RLOC = pv.vars['Router_1_RLOC']
MED_1_RLOC16 = pv.vars['MED_1_RLOC16']
SED_1_RLOC16 = pv.vars['SED_1_RLOC16']
MED_1_MLEID = Ipv6Addr(pv.vars['MED_1_MLEID'])
MED_1_OMR = Ipv6Addr(pv.vars['MED_1_OMR']) if pv.vars['MED_1_OMR'] else None
SED_1_MLEID = Ipv6Addr(pv.vars['SED_1_MLEID'])
SED_1_OMR = Ipv6Addr(pv.vars['SED_1_OMR']) if pv.vars['SED_1_OMR'] else None
# Step 1: All
# - Description (DIAG-4.1): Enable the devices in order. The remaining (child) devices are not yet activated.
# Leader configures its Network Data with an OMR prefix.
# - Pass Criteria:
# - Single Thread Network is formed with Leader and the DUT
print("Step 1: Single Thread Network is formed with Leader and the DUT.")
# Step 3: Router_1 (DUT)
# - Description (DIAG-4.1): Automatically responds with DIAG_GET.rsp containing the requested Diagnostic TLVs.
# - Pass Criteria:
# - The DUT MUST respond with DIAG_GET.rsp
# - The presence of each TLV (as requested in step 2) MUST be validated, with the exception of Max Child Timeout TLV
# (Type 19) which MAY be present.
print("Step 3: The DUT MUST respond with DIAG_GET.rsp.")
pkts.filter_ipv6_dst(Leader_RLOC).\
filter_coap_ack(consts.DIAG_GET_URI).\
filter(lambda p: check_step3(p)).\
must_next()
# Step 6: Router_1 (DUT)
# - Description (DIAG-4.1): Automatically responds with DIAG_GET.rsp containing the requested Diagnostic TLVs.
# - Pass Criteria:
# - The DUT MUST respond with DIAG_GET.rsp
# - Presence of each TLV (as requested in step 5) MUST be validated.
# - Value of Max Child Timeout TLV (Type 19) MUST be '312'.
# - In the MLE Counters TLV (Type 34)...
print("Step 6: The DUT MUST respond with DIAG_GET.rsp for MLE Counters.")
pkts.filter_ipv6_dst(Leader_RLOC).\
filter_coap_ack(consts.DIAG_GET_URI).\
filter(lambda p: check_step6(p)).\
must_next()
# Step 8: Router_1 (DUT)
# - Description (DIAG-4.1): Automatically responds with DIAG_GET.ans unicast with Child information for all its
# children.
# - Pass Criteria:
# - The DUT MUST respond with DIAG_GET.ans
# - The response MUST contain four (4) times a Child TLV (type 29)...
print("Step 8: The DUT MUST respond with DIAG_GET.ans with Child information.")
pkts.filter_ipv6_dst(Leader_RLOC).\
filter_coap_request(consts.DIAG_GET_ANS_URI).\
filter(lambda p: check_step8(p, pv.vars)).\
must_next()
# Step 10: Router_1 (DUT)
# - Description (DIAG-4.1): Automatically responds with DIAG_GET.ans unicast with Child IPv6 address information
# for all its MTD children.
# - Pass Criteria:
# - The DUT MUST respond with DIAG_GET.ans
# - The response MUST contain four (2) times a Child IPv6 Address TLV (type 30)...
print("Step 10: The DUT MUST respond with DIAG_GET.ans with Child IPv6 address information.")
pkts.filter_ipv6_dst(Leader_RLOC).\
filter_coap_request(consts.DIAG_GET_ANS_URI).\
filter(lambda p: check_step10(p, MED_1_RLOC16, MED_1_MLEID, MED_1_OMR,
SED_1_RLOC16, SED_1_MLEID, SED_1_OMR)).\
must_next()
# Step 12: Router_1 (DUT)
# - Description (DIAG-4.1): Automatically responds with DIAG_GET.ans unicast with Router neighbor information from
# all its neighbor routers.
# - Pass Criteria:
# - The DUT MUST respond with DIAG_GET.ans
# - THe Response MUST contain one (1) Router Neighbor TLV...
print("Step 12: The DUT MUST respond with DIAG_GET.ans with Router neighbor information.")
pkts.filter_ipv6_dst(Leader_RLOC).\
filter_coap_request(consts.DIAG_GET_ANS_URI).\
filter(lambda p: check_step12(p, Leader_RLOC16, Leader_EXTADDR, Leader_VERSION)).\
must_next()
if __name__ == '__main__':
verify_utils.run_main(verify)