diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 530971fc6..cc247ce91 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -145,6 +145,7 @@ ot_nexus_test(5_3_5 "cert;nexus") ot_nexus_test(5_3_6 "cert;nexus") ot_nexus_test(5_3_7 "cert;nexus") ot_nexus_test(5_3_8 "cert;nexus") +ot_nexus_test(5_3_9 "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 b7aece5ac..246388015 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -75,6 +75,7 @@ DEFAULT_TESTS=( "5_3_6" "5_3_7" "5_3_8" + "5_3_9" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_5_3_9.cpp b/tests/nexus/test_5_3_9.cpp new file mode 100644 index 000000000..87ad0319d --- /dev/null +++ b/tests/nexus/test_5_3_9.cpp @@ -0,0 +1,304 @@ +/* + * 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 "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" +#include "thread/network_data_local.hpp" +#include "thread/network_data_notifier.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 as a child and upgrade to a router, in milliseconds. + */ +static constexpr uint32_t kAttachToRouterTime = 200 * 1000; + +/** + * Time to advance for a node to join as a child, in milliseconds. + */ +static constexpr uint32_t kAttachAsChildTime = 5 * 1000; + +/** + * Time to advance for the network to stabilize after routers have attached, in milliseconds. + */ +static constexpr uint32_t kStabilizationTime = 10 * 1000; + +/** + * Time to wait for DHCPv6 GUA assignment, in milliseconds. + */ +static constexpr uint32_t kDhcpAssignmentTime = 100 * 1000; + +/** + * Time to wait for ICMPv6 Echo response, in milliseconds. + */ +static constexpr uint32_t kEchoResponseTime = 5 * 1000; + +/** + * Time to wait for Router ID to expire, in milliseconds. + */ +static constexpr uint32_t kRouterIdTimeout = 580 * 1000; + +/** + * Time to wait for Child ID to expire, in milliseconds. + */ +static constexpr uint32_t kChildTimeout = 600 * 1000; + +/** + * Payload size for ICMPv6 Echo Request. + */ +static constexpr uint16_t kEchoPayloadSize = 10; + +/** + * Hop limit for ICMPv6 Echo Request. + */ +static constexpr uint8_t kHopLimit = 64; + +/** + * Poll period for SED 1, in milliseconds. + */ +static constexpr uint32_t kSedPollPeriod = 100; + +static Ip6::Address GetGuaAddress(Node &aNode, const char *aPrefixString) +{ + Ip6::Prefix prefix; + Ip6::Address address; + bool found = false; + + SuccessOrQuit(prefix.FromString(aPrefixString)); + + for (const Ip6::Netif::UnicastAddress &addr : aNode.Get().GetUnicastAddresses()) + { + if (addr.GetAddress().MatchesPrefix(prefix)) + { + address = addr.GetAddress(); + found = true; + break; + } + } + + VerifyOrQuit(found, "did not get unicast address with prefix"); + + return address; +} + +void Test5_3_9(void) +{ + /** + * 5.3.9 Address Query - DHCP GUA + * + * 5.3.9.1 Topology + * - The Leader is configured as a Border Router with DHCPv6 server for prefixes 2001:: & 2002:: + * + * 5.3.9.2 Purpose & Description + * The purpose of this test case is to validate that the DUT is able to generate Address Query and Address + * Notification messages properly. + * + * Spec Reference | V1.1 Section | V1.3.0 Section + * ------------------------------------------------|---------------|--------------- + * Address Query / Proactive Address Notifications | 5.4.2 / 5.4.3 | 5.4.2 / 5.4.3 + */ + + Core nexus; + + Node &leader = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + Node &dut = nexus.CreateNode(); + Node &router3 = nexus.CreateNode(); + Node &sed1 = nexus.CreateNode(); + + leader.SetName("LEADER"); + router1.SetName("ROUTER_1"); + dut.SetName("DUT"); + router3.SetName("ROUTER_3"); + sed1.SetName("SED_1"); + + nexus.AdvanceTime(0); + + Instance::SetLogLevel(kLogLevelNote); + + /** + * Step 1: Leader + * - Description: Harness configures the device to be a DHCPv6 Border Router for prefixes 2001:: & 2002:: + * - Pass Criteria: N/A + */ + Log("Step 1: Leader"); + { + NetworkData::OnMeshPrefixConfig config; + + config.Clear(); + SuccessOrQuit(config.GetPrefix().FromString("2001::/64")); + config.mDhcp = true; + config.mOnMesh = true; + config.mPreferred = true; + config.mStable = true; + SuccessOrQuit(leader.Get().AddOnMeshPrefix(config)); + + config.Clear(); + SuccessOrQuit(config.GetPrefix().FromString("2002::/64")); + config.mDhcp = true; + config.mOnMesh = true; + config.mPreferred = true; + config.mStable = true; + SuccessOrQuit(leader.Get().AddOnMeshPrefix(config)); + + leader.Get().HandleServerDataUpdated(); + } + + leader.Form(); + nexus.AdvanceTime(kFormNetworkTime); + + /** + * Step 2: All + * - Description: Build the topology as described and begin the wireless sniffer. + * - Pass Criteria: N/A + */ + Log("Step 2: All"); + leader.AllowList(router1); + router1.AllowList(leader); + leader.AllowList(dut); + dut.AllowList(leader); + leader.AllowList(router3); + router3.AllowList(leader); + dut.AllowList(sed1); + sed1.AllowList(dut); + + router1.Join(leader); + dut.Join(leader); + router3.Join(leader); + + nexus.AdvanceTime(kAttachToRouterTime); + + sed1.Join(dut, Node::kAsSed); + nexus.AdvanceTime(kAttachAsChildTime); + + // Wait for DHCPv6 GUAs to be assigned. + nexus.AdvanceTime(kDhcpAssignmentTime); + + nexus.AdvanceTime(kStabilizationTime); + + SuccessOrQuit(sed1.Get().SetExternalPollPeriod(kSedPollPeriod)); + + // Save test info while all nodes have their addresses. + nexus.SaveTestInfo("test_5_3_9.json"); + + /** + * Step 3: SED_1 + * - Description: Harness instructs device to send an ICMPv6 Echo Request to Router_3 using GUA 2001:: address. + * - Pass Criteria: + * - The DUT MUST generate an Address Query Request on SED_1’s behalf to find Router_3 address. + * - The Address Query Request MUST be sent to the Realm-Local All-Routers multicast address (FF03::2). + * - CoAP URI-Path: NON POST coap:// + * - CoAP Payload: + * - Target EID TLV + * - The DUT MUST receive and process the incoming Address Query Response and forward the ICMPv6 Echo Request + * packet to SED_1. + */ + Log("Step 3: SED_1"); + nexus.SendAndVerifyEchoRequest(sed1, GetGuaAddress(router3, "2001::/64"), kEchoPayloadSize, kHopLimit, + kEchoResponseTime); + + /** + * Step 4: Router_1 + * - Description: Harness instructs device to send an ICMPv6 Echo Request to SED_1 using GUA 2001:: address. + * - Pass Criteria: + * - The DUT MUST respond to the Address Query Request with a properly formatted Address Notification Message: + * - CoAP URI-PATH: CON POST coap://[
]:MM/a/an + * - CoAP Payload: + * - Target EID TLV + * - RLOC16 TLV + * - ML-EID TLV + * - The IPv6 Source address MUST be the RLOC of the originator. + * - The IPv6 Destination address MUST be the RLOC of the destination. + */ + Log("Step 4: Router_1"); + nexus.SendAndVerifyEchoRequest(router1, GetGuaAddress(sed1, "2001::/64"), kEchoPayloadSize, kHopLimit, + kEchoResponseTime); + + /** + * Step 5: SED_1 + * - Description: Harness instructs device to send an ICMPv6 Echo Request to Router_3 using GUA 2001:: address. + * - Pass Criteria: + * - The DUT MUST NOT send an Address Query as Router_3 address should be cached. + * - The DUT MUST forward the ICMPv6 Echo Reply to SED_1. + */ + Log("Step 5: SED_1"); + nexus.SendAndVerifyEchoRequest(sed1, GetGuaAddress(router3, "2001::/64"), kEchoPayloadSize, kHopLimit, + kEchoResponseTime); + + /** + * Step 6: Router_2 (DUT) + * - Description: Harness silently powers off Router_3 and waits 580 seconds to allow Leader to expire its Router + * ID. Send an ICMPv6 Echo Request from MED_1 to Router_3 GUA 2001:: address. + * - Pass Criteria: + * - The DUT MUST update its address cache and remove all entries based on Router_3’s Router ID. + * - The DUT MUST send an Address Query to discover Router_3’s RLOC address. + */ + Log("Step 6: Router_2 (DUT)"); + Ip6::Address router3Gua = GetGuaAddress(router3, "2001::/64"); + router3.Reset(); + nexus.AdvanceTime(kRouterIdTimeout); + + sed1.SendEchoRequest(router3Gua, 0, kEchoPayloadSize); + nexus.AdvanceTime(kEchoResponseTime); + + /** + * Step 7: SED_1 + * - Description: Harness silently powers off SED_1 and waits to allow the DUT to timeout the child. Send two + * ICMPv6 Echo Requests from Router_1 to SED_1 GUA 2001:: address (one to clear the EID-to-RLOC Map Cache of the + * sender and the other to produce Address Query). + * - Pass Criteria: + * - The DUT MUST NOT respond with an Address Notification message. + */ + Log("Step 7: SED_1"); + Ip6::Address sed1Gua = GetGuaAddress(sed1, "2001::/64"); + sed1.Reset(); + nexus.AdvanceTime(kChildTimeout); + + router1.SendEchoRequest(sed1Gua, 1, kEchoPayloadSize); + nexus.AdvanceTime(kEchoResponseTime); + router1.SendEchoRequest(sed1Gua, 2, kEchoPayloadSize); + nexus.AdvanceTime(kEchoResponseTime); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::Test5_3_9(); + return 0; +} diff --git a/tests/nexus/verify_5_3_9.py b/tests/nexus/verify_5_3_9.py new file mode 100644 index 000000000..180d38940 --- /dev/null +++ b/tests/nexus/verify_5_3_9.py @@ -0,0 +1,181 @@ +#!/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 + +# CoAP types +COAP_TYPE_CON = 0 +COAP_TYPE_NON = 1 + + +def verify(pv): + # 5.3.9 Address Query - DHCP GUA + # + # 5.3.9.1 Topology + # - The Leader is configured as a Border Router with DHCPv6 server for prefixes 2001:: & 2002:: + # + # 5.3.9.2 Purpose & Description + # The purpose of this test case is to validate that the DUT is able to generate Address Query and Address + # Notification messages properly. + # + # Spec Reference | V1.1 Section | V1.3.0 Section + # ------------------------------------------------|---------------|--------------- + # Address Query / Proactive Address Notifications | 5.4.2 / 5.4.3 | 5.4.2 / 5.4.3 + + pkts = pv.pkts + pv.summary.show() + + ROUTER_1 = pv.vars['ROUTER_1'] + DUT = pv.vars['DUT'] + ROUTER_3 = pv.vars['ROUTER_3'] + SED_1 = pv.vars['SED_1'] + + def get_gua(ipaddrs, prefix): + for addr in ipaddrs: + if str(addr).lower().startswith(prefix.lower()): + return Ipv6Addr(addr) + return None + + ROUTER_3_GUA = get_gua(pv.vars['ROUTER_3_IPADDRS'], '2001:') + SED_1_GUA = get_gua(pv.vars['SED_1_IPADDRS'], '2001:') + + # Step 1: Leader + # - Description: Harness configures the device to be a DHCPv6 Border Router for prefixes 2001:: & 2002:: + # - Pass Criteria: N/A + print("Step 1: Leader") + + # Step 2: All + # - Description: Build the topology as described and begin the wireless sniffer. + # - Pass Criteria: N/A + print("Step 2: All") + + # Step 3: SED_1 + # - Description: Harness instructs device to send an ICMPv6 Echo Request to Router_3 using GUA 2001:: address. + # - Pass Criteria: + # - The DUT MUST generate an Address Query Request on SED_1’s behalf to find Router_3 address. + # - The Address Query Request MUST be sent to the Realm-Local All-Routers multicast address (FF03::2). + # - CoAP URI-Path: NON POST coap:// + # - CoAP Payload: + # - Target EID TLV + # - The DUT MUST receive and process the incoming Address Query Response and forward the ICMPv6 Echo Request + # packet to SED_1. + print("Step 3: SED_1") + # 1. Address Query Request from DUT + pkts.filter_coap_request(consts.ADDR_QRY_URI).\ + filter(lambda p: p.coap.tlv.target_eid == ROUTER_3_GUA).\ + must_next() + + # 2. Address Notification from Router_3 to DUT + pkts.filter_coap_request(consts.ADDR_NTF_URI).\ + filter(lambda p: p.coap.tlv.target_eid == ROUTER_3_GUA).\ + must_next() + + # 3. Echo Reply forwarded by DUT to SED_1 + pkts.filter_ping_reply().\ + must_next() + + # Step 4: Router_1 + # - Description: Harness instructs device to send an ICMPv6 Echo Request to SED_1 using GUA 2001:: address. + # - Pass Criteria: + # - The DUT MUST respond to the Address Query Request with a properly formatted Address Notification Message: + # - CoAP URI-PATH: CON POST coap://[
]:MM/a/an + # - CoAP Payload: + # - Target EID TLV + # - RLOC16 TLV + # - ML-EID TLV + # - The IPv6 Source address MUST be the RLOC of the originator. + # - The IPv6 Destination address MUST be the RLOC of the destination. + print("Step 4: Router_1") + # 1. Address Query Request from Router_1 + pkts.filter_coap_request(consts.ADDR_QRY_URI).\ + filter(lambda p: p.coap.tlv.target_eid == SED_1_GUA).\ + must_next() + + # 2. Address Notification from DUT to Router_1 + pkts.filter_coap_request(consts.ADDR_NTF_URI).\ + filter(lambda p: p.coap.tlv.target_eid == SED_1_GUA).\ + must_next() + + # Step 5: SED_1 + # - Description: Harness instructs device to send an ICMPv6 Echo Request to Router_3 using GUA 2001:: address. + # - Pass Criteria: + # - The DUT MUST NOT send an Address Query as Router_3 address should be cached. + # - The DUT MUST forward the ICMPv6 Echo Reply to SED_1. + print("Step 5: SED_1") + start_of_step_5 = pkts.index + pkts.filter_ping_reply().\ + must_next() + end_of_step_5 = pkts.index + + pkts.range(start_of_step_5, end_of_step_5).\ + filter_coap_request(consts.ADDR_QRY_URI).\ + filter(lambda p: p.coap.tlv.target_eid == ROUTER_3_GUA).\ + must_not_next() + + # Step 6: Router_2 (DUT) + # - Description: Harness silently powers off Router_3 and waits 580 seconds to allow Leader to expire its Router + # ID. Send an ICMPv6 Echo Request from MED_1 to Router_3 GUA 2001:: address. + # - Pass Criteria: + # - The DUT MUST update its address cache and remove all entries based on Router_3’s Router ID. + # - The DUT MUST send an Address Query to discover Router_3’s RLOC address. + print("Step 6: Router_2 (DUT)") + pkts.filter_coap_request(consts.ADDR_QRY_URI).\ + filter(lambda p: p.coap.tlv.target_eid == ROUTER_3_GUA).\ + must_next() + + # Step 7: SED_1 + # - Description: Harness silently powers off SED_1 and waits to allow the DUT to timeout the child. Send two + # ICMPv6 Echo Requests from Router_1 to SED_1 GUA 2001:: address (one to clear the EID-to-RLOC Map Cache of the + # sender and the other to produce Address Query). + # - Pass Criteria: + # - The DUT MUST NOT respond with an Address Notification message. + print("Step 7: SED_1") + # 1. Address Query Request from Router_1 + pkts.filter_coap_request(consts.ADDR_QRY_URI).\ + filter(lambda p: p.coap.tlv.target_eid == SED_1_GUA).\ + must_next() + + # 2. DUT must NOT respond with Address Notification + with pkts.save_index(): + pkts.filter_coap_request(consts.ADDR_NTF_URI).\ + filter(lambda p: p.coap.tlv.target_eid == SED_1_GUA).\ + must_not_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify)