From 272e9a738f8cda6ebd1cca76e91c165e7ed42226 Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Wed, 25 Feb 2026 12:57:01 -0600 Subject: [PATCH] [nexus] add test 9.2.14 PAN ID Query Requests (#12554) This commit adds Nexus test case 9.2.14 which verifies that the DUT properly accepts and processes PAN ID Query requests and responds when a conflict is found. Implementation details: - tests/nexus/test_9_2_14.cpp: C++ test execution logic. Implements a 4-node topology (Leader_1, Router_1, Commissioner, Leader_2). Leader_2 forms a separate network on a secondary channel with the same PAN ID. Test uses direct core method calls and sets log level to note. - tests/nexus/verify_9_2_14.py: Python pcap verification script. Verifies MGMT_PANID_QUERY requests (unicast and multicast) from the Commissioner and MGMT_PANID_CONFLICT responses from Router_1. Ensures correct TLVs are present in CoAP payloads. - tests/nexus/CMakeLists.txt: Added nexus_9_2_14 target. - tests/nexus/run_nexus_tests.sh: Added 9_2_14 to default test list. The test has been verified to pass in the Nexus simulation environment. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/run_nexus_tests.sh | 1 + tests/nexus/test_9_2_14.cpp | 272 +++++++++++++++++++++++++++++++++ tests/nexus/verify_9_2_14.py | 162 ++++++++++++++++++++ 4 files changed, 436 insertions(+) create mode 100644 tests/nexus/test_9_2_14.cpp create mode 100644 tests/nexus/verify_9_2_14.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 9128507ce..98f84fc80 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -205,6 +205,7 @@ ot_nexus_test(9_2_7 "cert;nexus") ot_nexus_test(9_2_8 "cert;nexus") ot_nexus_test(9_2_10 "cert;nexus") ot_nexus_test(9_2_11 "cert;nexus") +ot_nexus_test(9_2_14 "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 f1359a1da..8382439a8 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -141,6 +141,7 @@ DEFAULT_TESTS=( "9_2_8" "9_2_10" "9_2_11" + "9_2_14" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_9_2_14.cpp b/tests/nexus/test_9_2_14.cpp new file mode 100644 index 000000000..ee1cc919a --- /dev/null +++ b/tests/nexus/test_9_2_14.cpp @@ -0,0 +1,272 @@ +/* + * 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 "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 = 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 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 = 5000; + +/** + * Time to wait for ICMPv6 Echo response, in milliseconds. + */ +static constexpr uint32_t kEchoTimeout = 5000; + +/** + * Channel used for the main network. + */ +static constexpr uint8_t kPrimaryChannel = 11; + +/** + * Channel used for the separate network with same PAN ID. + */ +static constexpr uint8_t kSecondaryChannel = 20; + +/** + * PAN ID to use for both networks. + */ +static constexpr uint16_t kPanId = 0x1234; + +void Test9_2_14(void) +{ + /** + * 9.2.14 PAN ID Query Requests + * + * 9.2.14.1 Topology + * - Leader_2 forms a separate network on the Secondary channel, with the same PAN ID. + * + * 9.2.14.2 Purpose & Description + * The purpose of this test case is to ensure that the DUT is able to properly accept and process PAN ID + * Query requests and properly respond when a conflict is found. + * + * Spec Reference | V1.1 Section | V1.3.0 Section + * --------------------------|--------------|--------------- + * Avoiding PAN ID Conflicts | 8.7.9 | 8.7.9 + */ + + Core nexus; + + Node &leader1 = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + Node &commissioner = nexus.CreateNode(); + Node &leader2 = nexus.CreateNode(); + + leader1.SetName("LEADER_1"); + router1.SetName("ROUTER_1"); + commissioner.SetName("COMMISSIONER"); + leader2.SetName("LEADER_2"); + + nexus.AdvanceTime(0); + + Instance::SetLogLevel(kLogLevelNote); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: All"); + + /** + * Step 1: All + * - Description: Topology Ensure topology is formed correctly. + * - Pass Criteria: N/A + */ + + leader1.AllowList(router1); + leader1.AllowList(commissioner); + + router1.AllowList(leader1); + router1.AllowList(leader2); + + commissioner.AllowList(leader1); + + leader2.AllowList(router1); + + { + MeshCoP::Dataset::Info datasetInfo; + + SuccessOrQuit(datasetInfo.GenerateRandom(leader1.GetInstance())); + datasetInfo.Set(kPanId); + datasetInfo.Set(kPrimaryChannel); + leader1.Get().SaveLocal(datasetInfo); + + leader1.Get().Up(); + SuccessOrQuit(leader1.Get().Start()); + } + + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(leader1.Get().IsLeader()); + + router1.Join(leader1); + commissioner.Join(leader1); + + nexus.AdvanceTime(kJoinTime); + VerifyOrQuit(router1.Get().IsAttached()); + VerifyOrQuit(commissioner.Get().IsAttached()); + + while (!router1.Get().IsRouter() || !commissioner.Get().IsRouter()) + { + nexus.AdvanceTime(1000); + } + + SuccessOrQuit(commissioner.Get().Start(nullptr, nullptr, nullptr)); + nexus.AdvanceTime(kPetitionTime); + VerifyOrQuit(commissioner.Get().IsActive()); + + { + MeshCoP::Dataset::Info datasetInfo; + + SuccessOrQuit(leader1.Get().Read(datasetInfo)); + datasetInfo.Set(kSecondaryChannel); + + leader2.Get().SaveLocal(datasetInfo); + leader2.Get().Up(); + SuccessOrQuit(leader2.Get().Start()); + } + + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(leader2.Get().IsLeader()); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: Commissioner"); + + /** + * Step 2: Commissioner + * - Description: Harness instructs the Commissioner to send a unicast MGMT_PANID_QUERY.qry to Router_1. For + * DUT = Commissioner: Through implementation-specific means, the user instructs the DUT to send a + * MGMT_PANID_QUERY.qry to Router_1. + * - Pass Criteria: For DUT = Commissioner: The DUT MUST send a unicast MGMT_PANID_QUERY.qry unicast to + * Router_1: + * - CoAP Request URI: coap://[R]:MM/c/pq + * - CoAP Payload: + * - Commissioner Session ID TLV + * - Channel Mask TLV + * - PAN ID TLV + */ + + { + uint32_t channelMask = (1 << kSecondaryChannel); + SuccessOrQuit(commissioner.Get().GetPanIdQueryClient().SendQuery( + kPanId, channelMask, router1.Get().GetMeshLocalRloc(), nullptr, nullptr)); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: Router_1"); + + /** + * Step 3: Router_1 + * - Description: Automatically sends a MGMT_PANID_CONFLICT.ans reponse to the Commissioner. + * - Pass Criteria: For DUT = Router: The DUT MUST send MGMT_PANID_CONFLICT.ans to the Commissioner: + * - CoAP Request URI: coap://[Commissioner]:MM/c/pc + * - CoAP Payload: + * - Channel Mask TLV + * - PAN ID TLV + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Commissioner"); + + /** + * Step 4: Commissioner + * - Description: Harness instructs Commissioner to send MGMT_PANID_QUERY.qry to All Thread Node Multicast + * Address: FF33:0040:::1. For DUT = Commissioner: Through implementation-specific + * means, the user instructs the DUT to send a MGMT_PANID_QUERY.qry. + * - Pass Criteria: For DUT = Commissioner: The DUT MUST send a multicast MGMT_PANID_QUERY.qry + * - CoAP Request URI: coap://[Destination]:MM/c/pq + * - CoAP Payload: + * - Commissioner Session ID TLV + * - Channel Mask TLV + * - PAN ID TLV + */ + + { + uint32_t channelMask = (1 << kSecondaryChannel); + SuccessOrQuit(commissioner.Get().GetPanIdQueryClient().SendQuery( + kPanId, channelMask, commissioner.Get().GetRealmLocalAllThreadNodesAddress(), nullptr, nullptr)); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: Router_1"); + + /** + * Step 5: Router_1 + * - Description: Automatically sends a MGMT_PANID_CONFLICT.ans reponse to the Commissioner. + * - Pass Criteria: For DUT = Router: The DUT MUST send MGMT_PANID_CONFLICT.ans to the Commissioner: + * - CoAP Request URI: coap://[Commissioner]:MM/c/pc + * - CoAP Payload: + * - Channel Mask TLV + * - PAN ID TLV + */ + + nexus.AdvanceTime(kResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: Commissioner"); + + /** + * Step 6: Commissioner + * - 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(commissioner, router1.Get().GetMeshLocalEid(), 0, 64, kEchoTimeout); + + nexus.SaveTestInfo("test_9_2_14.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::Test9_2_14(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_9_2_14.py b/tests/nexus/verify_9_2_14.py new file mode 100644 index 000000000..4eec9aef8 --- /dev/null +++ b/tests/nexus/verify_9_2_14.py @@ -0,0 +1,162 @@ +#!/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.14 PAN ID Query Requests + # + # 9.2.14.1 Topology + # - Leader_2 forms a separate network on the Secondary channel, with the same PAN ID. + # + # 9.2.14.2 Purpose & Description + # The purpose of this test case is to ensure that the DUT is able to properly accept and process PAN ID + # Query requests and properly respond when a conflict is found. + # + # Spec Reference | V1.1 Section | V1.3.0 Section + # --------------------------|--------------|--------------- + # Avoiding PAN ID Conflicts | 8.7.9 | 8.7.9 + + pkts = pv.pkts + pv.summary.show() + + ROUTER_1 = pv.vars['ROUTER_1'] + COMMISSIONER = pv.vars['COMMISSIONER'] + ROUTER_1_RLOC = pv.vars['ROUTER_1_RLOC'] + COMMISSIONER_RLOC = pv.vars['COMMISSIONER_RLOC'] + COMMISSIONER_MLEID = pv.vars['COMMISSIONER_MLEID'] + ROUTER_1_MLEID = pv.vars['ROUTER_1_MLEID'] + + # Step 1: All + # - Description: Topology Ensure topology is formed correctly. + # - Pass Criteria: N/A + print("Step 1: All") + + # Step 2: Commissioner + # - Description: Harness instructs the Commissioner to send a unicast MGMT_PANID_QUERY.qry to Router_1. For + # DUT = Commissioner: Through implementation-specific means, the user instructs the DUT to send a + # MGMT_PANID_QUERY.qry to Router_1. + # - Pass Criteria: For DUT = Commissioner: The DUT MUST send a unicast MGMT_PANID_QUERY.qry unicast to + # Router_1: + # - CoAP Request URI: coap://[R]:MM/c/pq + # - CoAP Payload: + # - Commissioner Session ID TLV + # - Channel Mask TLV + # - PAN ID TLV + print("Step 2: Commissioner") + pkts.filter_wpan_src64(COMMISSIONER).\ + filter_ipv6_dst(ROUTER_1_RLOC).\ + filter_coap_request(consts.MGMT_PANID_QUERY).\ + filter(lambda p: { + consts.NM_COMMISSIONER_SESSION_ID_TLV, + consts.NM_CHANNEL_MASK_TLV, + consts.NM_PAN_ID_TLV + } <= set(p.coap.tlv.type)).\ + must_next() + + # Step 3: Router_1 + # - Description: Automatically sends a MGMT_PANID_CONFLICT.ans reponse to the Commissioner. + # - Pass Criteria: For DUT = Router: The DUT MUST send MGMT_ED_REPORT.ans to the Commissioner: + # - CoAP Request URI: coap://[Commissioner]:MM/c/pc + # - CoAP Payload: + # - Channel Mask TLV + # - PAN ID TLV + print("Step 3: Router_1") + pkts.filter_wpan_src64(ROUTER_1).\ + filter_ipv6_dst(COMMISSIONER_RLOC).\ + filter_coap_request(consts.MGMT_PANID_CONFLICT).\ + filter(lambda p: { + consts.NM_CHANNEL_MASK_TLV, + consts.NM_PAN_ID_TLV + } <= set(p.coap.tlv.type)).\ + must_next() + + # Step 4: Commissioner + # - Description: Harness instructs Commissioner to send MGMT_PANID_QUERY.qry to All Thread Node Multicast + # Address: FF33:0040:::1. For DUT = Commissioner: Through implementation-specific + # means, the user instructs the DUT to send a MGMT_PANID_QUERY.qry. + # - Pass Criteria: For DUT = Commissioner: The DUT MUST send a multicast MGMT_PANID_QUERY.qry + # - CoAP Request URI: coap://[Destination]:MM/c/pq + # - CoAP Payload: + # - Commissioner Session ID TLV + # - Channel Mask TLV + # - PAN ID TLV + print("Step 4: Commissioner") + pkts.filter_wpan_src64(COMMISSIONER).\ + filter_coap_request(consts.MGMT_PANID_QUERY).\ + filter(lambda p: str(p.ipv6.dst).startswith('ff33:0040:')).\ + filter(lambda p: { + consts.NM_COMMISSIONER_SESSION_ID_TLV, + consts.NM_CHANNEL_MASK_TLV, + consts.NM_PAN_ID_TLV + } <= set(p.coap.tlv.type)).\ + must_next() + + # Step 5: Router_1 + # - Description: Automatically sends a MGMT_PANID_CONFLICT.ans reponse to the Commissioner. + # - Pass Criteria: For DUT = Router: The DUT MUST send MGMT_PANID_CONFLICT.ans to the Commissioner: + # - CoAP Request URI: coap://[Commissioner]:MM/c/pc + # - CoAP Payload: + # - Channel Mask TLV + # - PAN ID TLV + print("Step 5: Router_1") + pkts.filter_wpan_src64(ROUTER_1).\ + filter_ipv6_dst(COMMISSIONER_RLOC).\ + filter_coap_request(consts.MGMT_PANID_CONFLICT).\ + filter(lambda p: { + consts.NM_CHANNEL_MASK_TLV, + consts.NM_PAN_ID_TLV + } <= set(p.coap.tlv.type)).\ + must_next() + + # Step 6: Commissioner + # - 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 6: Commissioner") + _pkt = pkts.filter_ping_request().\ + filter_ipv6_src(COMMISSIONER_MLEID).\ + filter_ipv6_dst(ROUTER_1_MLEID).\ + must_next() + pkts.filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).\ + filter_ipv6_src(ROUTER_1_MLEID).\ + filter_ipv6_dst(COMMISSIONER_MLEID).\ + must_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify)