From 244900a49aac26fed833b8d9972de989cbe9312b Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Tue, 17 Mar 2026 03:02:44 -0500 Subject: [PATCH] [nexus] add MATN-TC-12 test case for hop limit processing (#12701) This commit implements the MATN-TC-12 test case in the Nexus simulation framework to verify that a Primary BBR correctly decrements the IPv6 Hop Limit when forwarding multicast packets between the backbone link and the Thread network. Key implementation details include: - Implementation of MATN-TC-12 in C++ simulating a topology with a Border Router (BR_1 as DUT), a Thread Router, and a Host. - Enhancement of the Nexus platform to support hop limit processing: - Updated InfraIf::Receive to decrement Hop Limit when forwarding from the backbone to the Thread network. - Updated Node::HandleReceive to decrement Hop Limit when forwarding from the Thread network to the backbone. - Added support for simulating packets with Hop Limit 0 by setting mAllowZeroHopLimit in Node::SendEchoRequest. - Addition of a Python verification script to validate: - Multicast forwarding from backbone to Thread with decrement. - Multicast forwarding from Thread to backbone with decrement. - Dropping of packets with Hop Limit 1 (or 0) during forwarding. - Use of unique ICMPv6 identifiers to reliably distinguish between pings in different test steps. - Inclusion of the full test specification as inline comments in both the C++ and Python files. - Registration of the new test case in tests/nexus/CMakeLists.txt and tests/nexus/run_nexus_tests.sh. --- src/core/net/ip6.hpp | 1 - src/core/net/ip6_types.hpp | 5 + tests/nexus/CMakeLists.txt | 1 + tests/nexus/platform/nexus_core.cpp | 11 +- tests/nexus/platform/nexus_core.hpp | 2 +- tests/nexus/platform/nexus_infra_if.cpp | 13 +- tests/nexus/platform/nexus_infra_if.hpp | 3 +- tests/nexus/platform/nexus_node.cpp | 12 +- tests/nexus/platform/nexus_pcap.cpp | 7 +- tests/nexus/run_nexus_tests.sh | 1 + tests/nexus/test_1_1_5_3_10.cpp | 13 +- tests/nexus/test_1_1_9_2_7.cpp | 3 +- tests/nexus/test_1_2_LP_5_3_6.cpp | 14 +- tests/nexus/test_1_2_LP_7_2_2.cpp | 3 +- tests/nexus/test_1_2_MATN_TC_12.cpp | 349 ++++++++++++++++++++++++ tests/nexus/verify_1_2_MATN_TC_12.py | 276 +++++++++++++++++++ 16 files changed, 680 insertions(+), 34 deletions(-) create mode 100644 tests/nexus/test_1_2_MATN_TC_12.cpp create mode 100644 tests/nexus/verify_1_2_MATN_TC_12.py diff --git a/src/core/net/ip6.hpp b/src/core/net/ip6.hpp index a6815fd9a..733deb422 100644 --- a/src/core/net/ip6.hpp +++ b/src/core/net/ip6.hpp @@ -345,7 +345,6 @@ public: #endif private: - static constexpr uint8_t kDefaultHopLimit = OPENTHREAD_CONFIG_IP6_HOP_LIMIT_DEFAULT; static constexpr uint8_t kReassemblyTimeout = OPENTHREAD_CONFIG_IP6_REASSEMBLY_TIMEOUT; static constexpr uint16_t kMinimalMtu = 1280; diff --git a/src/core/net/ip6_types.hpp b/src/core/net/ip6_types.hpp index 8f24b840c..691d95fb9 100644 --- a/src/core/net/ip6_types.hpp +++ b/src/core/net/ip6_types.hpp @@ -62,6 +62,11 @@ static constexpr uint8_t kProtoIcmp6 = OT_IP6_PROTO_ICMP6; ///< ICMP for I static constexpr uint8_t kProtoNone = OT_IP6_PROTO_NONE; ///< No Next Header for IPv6 static constexpr uint8_t kProtoDstOpts = OT_IP6_PROTO_DST_OPTS; ///< Destination Options for IPv6 +/** + * The default hop limit value. + */ +static constexpr uint8_t kDefaultHopLimit = OPENTHREAD_CONFIG_IP6_HOP_LIMIT_DEFAULT; + /** * The max datagram length (in bytes) of an IPv6 message. */ diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index d496ad057..40f33a6d5 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -242,6 +242,7 @@ ot_nexus_test(1_2_MATN_TC_5 "cert;nexus") ot_nexus_test(1_2_MATN_TC_7 "cert;nexus") ot_nexus_test(1_2_MATN_TC_9 "cert;nexus") ot_nexus_test(1_2_MATN_TC_10 "cert;nexus") +ot_nexus_test(1_2_MATN_TC_12 "cert;nexus") # Misc tests ot_nexus_test(border_admitter "core;nexus") diff --git a/tests/nexus/platform/nexus_core.cpp b/tests/nexus/platform/nexus_core.cpp index a527e8f1f..bb74cb0a5 100644 --- a/tests/nexus/platform/nexus_core.cpp +++ b/tests/nexus/platform/nexus_core.cpp @@ -174,9 +174,14 @@ void Core::SaveTestInfo(const char *aFilename, Node *aLeaderNode) InfraIf::LinkLayerAddress mac; node.mInfraIf.GetLinkLayerAddress(mac); - fprintf(file, " \"%u\": \"%02x:%02x:%02x:%02x:%02x:%02x\"%s\n", node.GetInstance().GetId(), mac.mAddress[0], - mac.mAddress[1], mac.mAddress[2], mac.mAddress[3], mac.mAddress[4], mac.mAddress[5], - (&node == tail) ? "" : ","); + fprintf(file, " \"%u\": \"", node.GetInstance().GetId()); + + for (uint8_t i = 0; i < mac.mLength; i++) + { + fprintf(file, "%02x%s", mac.mAddress[i], (i + 1 == mac.mLength) ? "" : ":"); + } + + fprintf(file, "\"%s\n", (&node == tail) ? "" : ","); } fprintf(file, " },\n"); diff --git a/tests/nexus/platform/nexus_core.hpp b/tests/nexus/platform/nexus_core.hpp index e35afb99d..fe55e320a 100644 --- a/tests/nexus/platform/nexus_core.hpp +++ b/tests/nexus/platform/nexus_core.hpp @@ -69,7 +69,7 @@ public: void SendAndVerifyEchoRequest(Node &aSender, const Ip6::Address &aDestination, uint16_t aPayloadSize = 0, - uint8_t aHopLimit = 64, + uint8_t aHopLimit = Ip6::kDefaultHopLimit, uint32_t aResponseTimeout = 1000); //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/nexus/platform/nexus_infra_if.cpp b/tests/nexus/platform/nexus_infra_if.cpp index 3943b10eb..88b75ce6a 100644 --- a/tests/nexus/platform/nexus_infra_if.cpp +++ b/tests/nexus/platform/nexus_infra_if.cpp @@ -249,7 +249,8 @@ void InfraIf::SendIp6(const Ip6::Address &aSrcAddress, void InfraIf::SendEchoRequest(const Ip6::Address &aSrcAddress, const Ip6::Address &aDestAddress, uint16_t aIdentifier, - uint16_t aPayloadSize) + uint16_t aPayloadSize, + uint8_t aHopLimit) { Message *message; Ip6::Header ip6Header; @@ -262,7 +263,7 @@ void InfraIf::SendEchoRequest(const Ip6::Address &aSrcAddress, ip6Header.InitVersionTrafficClassFlow(); ip6Header.SetPayloadLength(sizeof(Ip6::Icmp::Header) + aPayloadSize); ip6Header.SetNextHeader(Ip6::kProtoIcmp6); - ip6Header.SetHopLimit(64); + ip6Header.SetHopLimit(aHopLimit); ip6Header.SetSource(aSrcAddress); ip6Header.SetDestination(aDestAddress); @@ -384,11 +385,15 @@ void InfraIf::Receive(Node &aSrcNode, const Ip6::Header &aHeader, Message &aMess // We also deliver generic IPv6 packets to the stack if they are NOT ICMPv6 ND packets. // (ND packets were already delivered via otPlatInfraIfRecvIcmp6Nd above). OwnedPtr messagePtr; + Ip6::Header updatedHeader = aHeader; - messagePtr.Reset(node.Get().NewMessage()); + VerifyOrExit(updatedHeader.GetHopLimit() > 1); + updatedHeader.SetHopLimit(updatedHeader.GetHopLimit() - 1); + + messagePtr.Reset(aMessage.Clone()); VerifyOrQuit(messagePtr != nullptr); - SuccessOrQuit(messagePtr->AppendBytesFromMessage(aMessage, 0, aMessage.GetLength())); + messagePtr->Write(0, updatedHeader); messagePtr->SetOrigin(Message::kOriginHostUntrusted); messagePtr->SetLoopbackToHostAllowed(false); diff --git a/tests/nexus/platform/nexus_infra_if.hpp b/tests/nexus/platform/nexus_infra_if.hpp index 368ae457d..02ac21400 100644 --- a/tests/nexus/platform/nexus_infra_if.hpp +++ b/tests/nexus/platform/nexus_infra_if.hpp @@ -65,7 +65,8 @@ public: void SendEchoRequest(const Ip6::Address &aSrcAddress, const Ip6::Address &aDestAddress, uint16_t aIdentifier, - uint16_t aPayloadSize); + uint16_t aPayloadSize, + uint8_t aHopLimit = Ip6::kDefaultHopLimit); void SendUdp(const Ip6::Address &aSrcAddress, const Ip6::Address &aDestAddress, uint16_t aSourcePort, diff --git a/tests/nexus/platform/nexus_node.cpp b/tests/nexus/platform/nexus_node.cpp index 3181d8c65..2147f7da4 100644 --- a/tests/nexus/platform/nexus_node.cpp +++ b/tests/nexus/platform/nexus_node.cpp @@ -124,6 +124,7 @@ void Node::SendEchoRequest(const Ip6::Address &aDestination, messageInfo.SetPeerAddr(aDestination); messageInfo.SetHopLimit(aHopLimit); + messageInfo.mAllowZeroHopLimit = true; if (aSrcAddress != nullptr) { @@ -145,21 +146,24 @@ void Node::HandleIp6Receive(otMessage *aMessage, void *aContext) void Node::HandleReceive(otMessage *aMessage) { - uint16_t length = otMessageGetLength(aMessage); - uint8_t buffer[1500]; - const Ip6::Header *header; + uint16_t length = otMessageGetLength(aMessage); + uint8_t buffer[1500]; + Ip6::Header *header; VerifyOrExit(length <= sizeof(buffer)); VerifyOrQuit(otMessageRead(aMessage, 0, buffer, length) == length); VerifyOrExit(length >= sizeof(Ip6::Header)); - header = reinterpret_cast(buffer); + header = reinterpret_cast(buffer); // Forward packets to InfraIf if they are intended for the backbone. // We avoid forwarding link-local and realm-local scope packets. VerifyOrExit(mInfraIf.IsInitialized()); VerifyOrExit(header->GetHopLimit() > 1); + + header->SetHopLimit(header->GetHopLimit() - 1); + VerifyOrExit(header->GetDestination().GetScope() > Ip6::Address::kRealmLocalScope); Core::Get().SetActiveNode(this); diff --git a/tests/nexus/platform/nexus_pcap.cpp b/tests/nexus/platform/nexus_pcap.cpp index b08a3f59d..871c8b892 100644 --- a/tests/nexus/platform/nexus_pcap.cpp +++ b/tests/nexus/platform/nexus_pcap.cpp @@ -27,6 +27,7 @@ */ #include "nexus_pcap.hpp" +#include "nexus_utils.hpp" #include "common/clearable.hpp" #include "common/code_utils.hpp" #include "common/encoding.hpp" @@ -226,8 +227,10 @@ void Pcap::WritePacket(const otPlatInfraIfLinkLayerAddress &aSrcAddr, VerifyOrExit(mFile != nullptr); ClearAllBytes(ethHeader); - memcpy(ethHeader.mDst, aDstAddr.mAddress, 6); - memcpy(ethHeader.mSrc, aSrcAddr.mAddress, 6); + VerifyOrQuit(aDstAddr.mLength >= sizeof(ethHeader.mDst), "Destination MAC address length is less than expected"); + memcpy(ethHeader.mDst, aDstAddr.mAddress, sizeof(ethHeader.mDst)); + VerifyOrQuit(aSrcAddr.mLength >= sizeof(ethHeader.mSrc), "Source MAC address length is less than expected"); + memcpy(ethHeader.mSrc, aSrcAddr.mAddress, sizeof(ethHeader.mSrc)); ethHeader.mType = BigEndian::HostSwap16(kEtherTypeIPv6); packetLen = sizeof(ethHeader) + aLength; diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index bf4c4643b..360e69ec4 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -177,6 +177,7 @@ DEFAULT_TESTS=( "1_2_MATN_TC_7" "1_2_MATN_TC_9" "1_2_MATN_TC_10" + "1_2_MATN_TC_12" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_1_1_5_3_10.cpp b/tests/nexus/test_1_1_5_3_10.cpp index 95409abc2..407036952 100644 --- a/tests/nexus/test_1_1_5_3_10.cpp +++ b/tests/nexus/test_1_1_5_3_10.cpp @@ -66,11 +66,6 @@ static constexpr uint32_t kStabilizationTime = 10 * 1000; */ static constexpr uint32_t kRouterIdExpirationTimeInSec = 580; -/** - * Default hop limit for IPv6 packets. - */ -static constexpr uint8_t kDefaultHopLimit = 64; - /** * ICMPv6 Echo Request identifiers used in different steps. */ @@ -212,7 +207,8 @@ void Test5_3_10(void) * packet to Router_1. */ - nexus.SendAndVerifyEchoRequest(med1, router1.FindMatchingAddress(kPrefix1), 0, kDefaultHopLimit, kEchoResponseTime); + nexus.SendAndVerifyEchoRequest(med1, router1.FindMatchingAddress(kPrefix1), 0, Ip6::kDefaultHopLimit, + kEchoResponseTime); Log("Step 4: Border Router"); @@ -230,7 +226,7 @@ void Test5_3_10(void) * - The IPv6 Destination address MUST be the RLOC of the destination. */ - nexus.SendAndVerifyEchoRequest(br, med1.FindMatchingAddress(kPrefix1), 0, kDefaultHopLimit, kEchoResponseTime); + nexus.SendAndVerifyEchoRequest(br, med1.FindMatchingAddress(kPrefix1), 0, Ip6::kDefaultHopLimit, kEchoResponseTime); Log("Step 5: MED_1"); @@ -242,7 +238,8 @@ void Test5_3_10(void) * - The DUT MUST forward the ICMPv6 Echo Reply to MED_1. */ - nexus.SendAndVerifyEchoRequest(med1, router1.FindMatchingAddress(kPrefix1), 0, kDefaultHopLimit, kEchoResponseTime); + nexus.SendAndVerifyEchoRequest(med1, router1.FindMatchingAddress(kPrefix1), 0, Ip6::kDefaultHopLimit, + kEchoResponseTime); nexus.SaveTestInfo("test_1_1_5_3_10.json"); diff --git a/tests/nexus/test_1_1_9_2_7.cpp b/tests/nexus/test_1_1_9_2_7.cpp index d629851d9..a18274c04 100644 --- a/tests/nexus/test_1_1_9_2_7.cpp +++ b/tests/nexus/test_1_1_9_2_7.cpp @@ -652,7 +652,8 @@ void Test9_2_7(void) * - Pass Criteria: The DUT MUST respond with an ICMPv6 Echo Reply. */ - nexus.SendAndVerifyEchoRequest(router, leader.Get().GetMeshLocalEid(), 0, 0, kEchoTimeout); + nexus.SendAndVerifyEchoRequest(router, leader.Get().GetMeshLocalEid(), 0, Ip6::kDefaultHopLimit, + kEchoTimeout); nexus.SaveTestInfo("test_1_1_9_2_7.json"); } diff --git a/tests/nexus/test_1_2_LP_5_3_6.cpp b/tests/nexus/test_1_2_LP_5_3_6.cpp index 0b76d238e..00413e178 100644 --- a/tests/nexus/test_1_2_LP_5_3_6.cpp +++ b/tests/nexus/test_1_2_LP_5_3_6.cpp @@ -64,11 +64,6 @@ static constexpr uint32_t kStabilizationTime = 10 * 1000; */ static constexpr uint16_t kEchoPayloadSize = 10; -/** - * Default hop limit for IPv6 packets. - */ -static constexpr uint8_t kDefaultHopLimit = 64; - /** * ICMPv6 Echo Request identifier for initial connectivity check. */ @@ -189,7 +184,8 @@ void Test1_2_LP_5_3_6(void) */ Log("Step 4: SSED_1"); - leader.SendEchoRequest(ssed1.Get().GetMeshLocalEid(), kEchoIdInitial, kEchoPayloadSize, kDefaultHopLimit); + leader.SendEchoRequest(ssed1.Get().GetMeshLocalEid(), kEchoIdInitial, kEchoPayloadSize, + Ip6::kDefaultHopLimit); nexus.AdvanceTime(kStabilizationTime); /** @@ -228,7 +224,8 @@ void Test1_2_LP_5_3_6(void) */ Log("Step 7: Leader"); - leader.SendEchoRequest(ssed1.Get().GetMeshLocalEid(), kEchoIdAsMed, kEchoPayloadSize, kDefaultHopLimit); + leader.SendEchoRequest(ssed1.Get().GetMeshLocalEid(), kEchoIdAsMed, kEchoPayloadSize, + Ip6::kDefaultHopLimit); nexus.AdvanceTime(kStabilizationTime); /** @@ -284,7 +281,8 @@ void Test1_2_LP_5_3_6(void) */ Log("Step 12: SSED_1"); - leader.SendEchoRequest(ssed1.Get().GetMeshLocalEid(), kEchoIdAsSsed, kEchoPayloadSize, kDefaultHopLimit); + leader.SendEchoRequest(ssed1.Get().GetMeshLocalEid(), kEchoIdAsSsed, kEchoPayloadSize, + Ip6::kDefaultHopLimit); nexus.AdvanceTime(kStabilizationTime); nexus.SaveTestInfo("test_1_2_LP_5_3_6.json"); diff --git a/tests/nexus/test_1_2_LP_7_2_2.cpp b/tests/nexus/test_1_2_LP_7_2_2.cpp index 4ed98be80..a0f43f7de 100644 --- a/tests/nexus/test_1_2_LP_7_2_2.cpp +++ b/tests/nexus/test_1_2_LP_7_2_2.cpp @@ -255,7 +255,8 @@ void Test1_2_LP_7_2_2(void) Node *nodes[] = {&sed1, &sed2, &sed3, &ssed1, &ssed2, &ssed3}; for (Node *node : nodes) { - nexus.SendAndVerifyEchoRequest(*node, leader.Get().GetLinkLocalAddress(), 0, 0, kEchoTimeout); + nexus.SendAndVerifyEchoRequest(*node, leader.Get().GetLinkLocalAddress(), 0, Ip6::kDefaultHopLimit, + kEchoTimeout); } /** diff --git a/tests/nexus/test_1_2_MATN_TC_12.cpp b/tests/nexus/test_1_2_MATN_TC_12.cpp new file mode 100644 index 000000000..1fe069709 --- /dev/null +++ b/tests/nexus/test_1_2_MATN_TC_12.cpp @@ -0,0 +1,349 @@ +/* + * 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 "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 = 10 * 1000; + +/** + * Time to advance for a node to join as a router, in milliseconds. + */ +static constexpr uint32_t kAttachToRouterTime = 200 * 1000; + +/** + * Time to advance for the network to stabilize, in milliseconds. + */ +static constexpr uint32_t kStabilizationTime = 10 * 1000; + +/** + * ICMPv6 Echo Request identifiers. + */ +static constexpr uint16_t kEchoIdentifier1 = 0x1231; +static constexpr uint16_t kEchoIdentifier2 = 0x1232; +static constexpr uint16_t kEchoIdentifier3 = 0x1233; +static constexpr uint16_t kEchoIdentifier4 = 0x1234; +static constexpr uint16_t kEchoIdentifier5 = 0x1235; +static constexpr uint16_t kEchoIdentifier6 = 0x1236; + +/** + * ICMPv6 Echo Request payload size. + */ +static constexpr uint16_t kEchoPayloadSize = 130; + +/** + * Multicast address (admin-local). + */ +static const char kAdminLocalMcast[] = "ff04::1234:777a:1"; + +/** + * Multicast address (site-local). + */ +static const char kSiteLocalMcast[] = "ff05::1234:777a:1"; + +/** + * IPv6 Hop Limit values. + */ +static constexpr uint8_t kHopLimit59 = 59; +static constexpr uint8_t kHopLimit1 = 1; +static constexpr uint8_t kHopLimit159 = 159; +static constexpr uint8_t kHopLimit2 = 2; +static constexpr uint8_t kHopLimit0 = 0; + +void TestMatnTc12(void) +{ + /** + * 5.10.9 MATN-TC-12: Hop limit processing + * + * 5.10.9.1 Topology + * - BR_1 (DUT) + * - Router + * - Host + * + * 5.10.9.2 Purpose & Description + * The purpose of this test case is to verify that a Primary BBR correctly decrements the Hop Limit field by 1 if + * packet is forwarded with MPL, and if forwarded from Thread Network (using MPL) to LAN. This test also verifies + * that the BR drops a packet with Hop Limit 0. It also checks the use of IPv6 packets that span multiple 6LoWPAN + * fragments. + * + * Spec Reference | V1.1 Section | V1.2 Section | V1.3.0 Section + * ---------------|--------------|--------------|--------------- + * Multicast | N/A | 5.10.9 | 5.10.9 + */ + + Core nexus; + Node &br1 = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + Node &host = nexus.CreateNode(); + Ip6::Address ma1; + Ip6::Address ma2; + const Ip6::Address *hostUla; + + br1.SetName("BR_1"); + router.SetName("Router"); + host.SetName("Host"); + + SuccessOrQuit(ma1.FromString(kAdminLocalMcast)); + SuccessOrQuit(ma2.FromString(kSiteLocalMcast)); + + nexus.AdvanceTime(0); + + Instance::SetLogLevel(kLogLevelNote); + + Log("Step 0: Topology formation – BR_1 (DUT), Router"); + + /** + * Step 0 + * - Device: N/A + * - Description: Topology formation – BR_1 (DUT), Router + * - Pass Criteria: + * - N/A + */ + br1.AllowList(router); + router.AllowList(br1); + + br1.Form(); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(br1.Get().IsLeader()); + + br1.Get().Init(1, true); + br1.Get().Init(); + SuccessOrQuit(br1.Get().SetEnabled(true)); + br1.Get().SetEnabled(true); + + router.Join(br1, Node::kAsFtd); + nexus.AdvanceTime(kAttachToRouterTime); + + VerifyOrQuit(router.Get().IsRouter()); + + nexus.AdvanceTime(kStabilizationTime); + hostUla = &host.mInfraIf.FindMatchingAddress("fd00::/8"); + + nexus.AddTestVar("MA1", kAdminLocalMcast); + nexus.AddTestVar("MA2", kSiteLocalMcast); + + VerifyOrQuit(br1.Get().IsPrimary()); + Log("BR_1 is Primary BBR"); + + Log("Step 1: Harness instructs the device to register a multicast address, MA1"); + + /** + * Step 1 + * - Device: Router + * - Description: Harness instructs the device to register a multicast address, MA1 + * - Pass Criteria: + * - N/A + */ + SuccessOrQuit(router.Get().SubscribeExternalMulticast(ma1)); + nexus.AdvanceTime(kStabilizationTime); + + Log("Step 2: Harness instructs the device to multicast a ICMPv6 Echo (ping) Request packet to the multicast " + "address, MA1, with the IPv6 Hop Limit field set to 59. The size of the payload is 130 bytes."); + + /** + * Step 2 + * - Device: Host + * - Description: Harness instructs the device to multicast a ICMPv6 Echo (ping) Request packet to the multicast + * address, MA1, with the IPv6 Hop Limit field set to 59. The size of the payload is 130 bytes. + * - Pass Criteria: + * - N/A + */ + host.mInfraIf.SendEchoRequest(*hostUla, ma1, kEchoIdentifier1, kEchoPayloadSize, kHopLimit59); + nexus.AdvanceTime(kStabilizationTime); + + Log("Step 3: Automatically forwards the ping request packet onto the Thread Network"); + + /** + * Step 3 + * - Device: BR_1 (DUT) + * - Description: Automatically forwards the ping request packet onto the Thread Network + * - Pass Criteria: + * - The DUT MUST forward the ICMPv6 Echo (ping) Request packet to Router_1 as an MPL packet encapsulating the + * IPv6 packet with the Hop Limit field of the inner packet set to 58. + */ + + Log("Step 4: Receives the multicast ping request packet."); + + /** + * Step 4 + * - Device: Router + * - Description: Receives the multicast ping request packet. + * - Pass Criteria: + * - N/A + */ + + Log("Step 5: Harness instructs the device to multicast a ICMPv6 Echo (ping) Request packet to the multicast " + "address, MA1, with the IPv6 Hop Limit field set to 1. The size of the payload is 130 bytes."); + + /** + * Step 5 + * - Device: Host + * - Description: Harness instructs the device to multicast a ICMPv6 Echo (ping) Request packet to the multicast + * address, MA1, with the IPv6 Hop Limit field set to 1. The size of the payload is 130 bytes. + * - Pass Criteria: + * - N/A + */ + host.mInfraIf.SendEchoRequest(*hostUla, ma1, kEchoIdentifier2, kEchoPayloadSize, kHopLimit1); + nexus.AdvanceTime(kStabilizationTime); + + Log("Step 6: Does not forward the ping request packet."); + + /** + * Step 6 + * - Device: BR_1 (DUT) + * - Description: Does not forward the ping request packet. + * - Pass Criteria: + * - The DUT MUST NOT forward the ICMPv6 Echo (ping) Request packet to the Thread Network. + */ + + Log("Step 7: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet encapsulated in an MPL " + "packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet set to " + "159. The size of the payload is 130 bytes."); + + /** + * Step 7 + * - Device: Router + * - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet encapsulated in an MPL + * packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet set to 159. + * The size of the payload is 130 bytes. + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(ma2, kEchoIdentifier3, kEchoPayloadSize, kHopLimit159); + nexus.AdvanceTime(kStabilizationTime); + + Log("Step 8: Automatically forwards the ping request packet to the LAN."); + + /** + * Step 8 + * - Device: BR_1 (DUT) + * - Description: Automatically forwards the ping request packet to the LAN. + * - Pass Criteria: + * - The DUT MUST forward the multicast ICMPv6 Echo (ping) Request packet to the LAN with the Hop Limit field set + * to 158. + */ + + Log("Step 9: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet encapsulated in an MPL " + "multicast packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet " + "set to 2. The size of the payload is 130 bytes."); + + /** + * Step 9 + * - Device: Router + * - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet encapsulated in an MPL + * multicast packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet set + * to 2. The size of the payload is 130 bytes. + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(ma2, kEchoIdentifier4, kEchoPayloadSize, kHopLimit2); + nexus.AdvanceTime(kStabilizationTime); + + Log("Step 10: Automatically forwards the ping request packet to the LAN."); + + /** + * Step 10 + * - Device: BR_1 (DUT) + * - Description: Automatically forwards the ping request packet to the LAN. + * - Pass Criteria: + * - The DUT MUST forward the multicast ICMPv6 Echo (ping) Request packet to the LAN with the Hop Limit field set + * to 1. + */ + + Log("Step 11: Harness instructs the device to send an ICMPv6 Echo (ping) Request packet encapsulated in an MPL " + "multicast packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet " + "set to 1. The size of the payload is 130 bytes."); + + /** + * Step 11 + * - Device: Router + * - Description: Harness instructs the device to send an ICMPv6 Echo (ping) Request packet encapsulated in an MPL + * multicast packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet set + * to 1. The size of the payload is 130 bytes. + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(ma2, kEchoIdentifier5, kEchoPayloadSize, kHopLimit1); + nexus.AdvanceTime(kStabilizationTime); + + Log("Step 12: Does not forward the ping request packet to the LAN."); + + /** + * Step 12 + * - Device: BR_1 (DUT) + * - Description: Does not forward the ping request packet to the LAN. + * - Pass Criteria: + * - The DUT MUST NOT forward the ICMPv6 Echo (ping) Request packet to the LAN. + */ + + Log("Step 13: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet encapsulated in an MPL " + "multicast packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet " + "set to 0."); + + /** + * Step 13 + * - Device: Router + * - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet encapsulated in an MPL + * multicast packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet set + * to 0. + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(ma2, kEchoIdentifier6, kEchoPayloadSize, kHopLimit0); + nexus.AdvanceTime(kStabilizationTime); + + Log("Step 14: Does not forward the ping request packet to the LAN."); + + /** + * Step 14 + * - Device: BR_1 (DUT) + * - Description: Does not forward the ping request packet to the LAN. + * - Pass Criteria: + * - The DUT MUST NOT forward the ICMPv6 Echo (ping) Request packet to the LAN. + */ + + nexus.SaveTestInfo("test_1_2_MATN_TC_12.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestMatnTc12(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_1_2_MATN_TC_12.py b/tests/nexus/verify_1_2_MATN_TC_12.py new file mode 100644 index 000000000..fce658375 --- /dev/null +++ b/tests/nexus/verify_1_2_MATN_TC_12.py @@ -0,0 +1,276 @@ +#!/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 + + +def verify(pv): + # 5.10.9 MATN-TC-12: Hop limit processing + # + # 5.10.9.1 Topology + # - BR_1 (DUT) + # - Router + # - Host + # + # 5.10.9.2 Purpose and Description + # The purpose of this test case is to verify that a Primary BBR correctly decrements the Hop Limit field by 1 if + # packet is forwarded with MPL, and if forwarded from Thread Network (using MPL) to LAN. This test also verifies + # that the BR drops a packet with Hop Limit 0. It also checks the use of IPv6 packets that span multiple 6LoWPAN + # fragments. + # + # Spec Reference | V1.1 Section | V1.2 Section | V1.3.0 Section + # ---------------|--------------|--------------|--------------- + # Multicast | N/A | 5.10.9 | 5.10.9 + + pkts = pv.pkts + vars = pv.vars + pv.summary.show() + + BR_1 = vars['BR_1'] + ROUTER = vars['Router'] + MA1 = Ipv6Addr(vars['MA1']) + MA2 = Ipv6Addr(vars['MA2']) + ECHO_ID1 = 0x1231 + ECHO_ID2 = 0x1232 + ECHO_ID3 = 0x1233 + ECHO_ID4 = 0x1234 + ECHO_ID5 = 0x1235 + ECHO_ID6 = 0x1236 + + HL_59 = 59 + HL_58 = 58 + HL_1 = 1 + HL_159 = 159 + HL_158 = 158 + HL_2 = 2 + HL_0 = 0 + + # Step 0 + # - Device: N/A + # - Description: Topology formation – BR_1 (DUT), Router + # - Pass Criteria: + # - N/A + print("Step 0: Topology formation – BR_1 (DUT), Router") + + # Step 1 + # - Device: Router + # - Description: Harness instructs the device to register a multicast address, MA1 + # - Pass Criteria: + # - N/A + print("Step 1: Router registers a multicast address, MA1") + pkts.filter_wpan_src64(ROUTER).\ + filter_coap_request("/n/mr").\ + filter(lambda p: MA1 in p.coap.tlv.ipv6_address).\ + must_next() + + # Step 2 + # - Device: Host + # - Description: Harness instructs the device to multicast a ICMPv6 Echo (ping) Request packet to the multicast + # address, MA1, with the IPv6 Hop Limit field set to 59. The size of the payload is 130 bytes. + # - Pass Criteria: + # - N/A + print("Step 2: Host multicasts a ICMPv6 Echo Request to MA1, HL=59") + pkts.filter_eth_src(vars['Host_ETH']).\ + filter_ping_request(identifier=ECHO_ID1).\ + filter(lambda p: p.ipv6.dst == MA1).\ + filter(lambda p: p.ipv6.hlim == HL_59).\ + must_next() + + # Step 3 + # - Device: BR_1 (DUT) + # - Description: Automatically forwards the ping request packet onto the Thread Network + # - Pass Criteria: + # - The DUT MUST forward the ICMPv6 Echo (ping) Request packet to Router_1 as an MPL packet encapsulating the + # IPv6 packet with the Hop Limit field of the inner packet set to 58. + print("Step 3: BR_1 forwards to Thread as MPL, inner HL=58") + pkts.filter_wpan_src64(BR_1).\ + filter_AMPLFMA().\ + filter(lambda p: p.ipv6inner.dst == MA1).\ + filter(lambda p: p.ipv6inner.hlim == HL_58).\ + filter(lambda p: p.icmpv6.echo.identifier == ECHO_ID1).\ + filter(lambda p: p.lowpan.frag).\ + must_next() + + # Step 4 + # - Device: Router + # - Description: Receives the multicast ping request packet. + # - Pass Criteria: + # - N/A + print("Step 4: Router receives the multicast ping request packet.") + + # Step 5 + # - Device: Host + # - Description: Harness instructs the device to multicast a ICMPv6 Echo (ping) Request packet to the multicast + # address, MA1, with the IPv6 Hop Limit field set to 1. The size of the payload is 130 bytes. + # - Pass Criteria: + # - N/A + print("Step 5: Host multicasts a ICMPv6 Echo Request to MA1, HL=1") + pkts.filter_eth_src(vars['Host_ETH']).\ + filter_ping_request(identifier=ECHO_ID2).\ + filter(lambda p: p.ipv6.dst == MA1).\ + filter(lambda p: p.ipv6.hlim == HL_1).\ + must_next() + + # Step 6 + # - Device: BR_1 (DUT) + # - Description: Does not forward the ping request packet. + # - Pass Criteria: + # - The DUT MUST NOT forward the ICMPv6 Echo (ping) Request packet to the Thread Network. + print("Step 6: BR_1 MUST NOT forward to Thread") + pkts.copy().\ + filter_wpan_src64(BR_1).\ + filter_AMPLFMA().\ + filter(lambda p: p.ipv6inner.dst == MA1).\ + filter(lambda p: p.icmpv6.echo.identifier == ECHO_ID2).\ + must_not_next() + + # Step 7 + # - Device: Router + # - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet encapsulated in an MPL + # packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet set to 159. + # The size of the payload is 130 bytes. + # - Pass Criteria: + # - N/A + print("Step 7: Router sends Echo Request in MPL to MA2, inner HL=159") + pkts.filter_wpan_src64(ROUTER).\ + filter_AMPLFMA().\ + filter(lambda p: p.ipv6inner.dst == MA2).\ + filter(lambda p: p.ipv6inner.hlim == HL_159).\ + filter(lambda p: p.icmpv6.echo.identifier == ECHO_ID3).\ + filter(lambda p: p.lowpan.frag).\ + must_next() + + # Step 8 + # - Device: BR_1 (DUT) + # - Description: Automatically forwards the ping request packet to the LAN. + # - Pass Criteria: + # - The DUT MUST forward the multicast ICMPv6 Echo (ping) Request packet to the LAN with the Hop Limit field set + # to 158. + print("Step 8: BR_1 forwards to LAN, HL=158") + pkts.filter_eth_src(vars['BR_1_ETH']).\ + filter_ping_request(identifier=ECHO_ID3).\ + filter(lambda p: p.ipv6.dst == MA2).\ + filter(lambda p: p.ipv6.hlim == HL_158).\ + must_next() + + # Step 9 + # - Device: Router + # - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet encapsulated in an MPL + # multicast packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet set + # to 2. The size of the payload is 130 bytes. + # - Pass Criteria: + # - N/A + print("Step 9: Router sends Echo Request in MPL to MA2, inner HL=2") + pkts.filter_wpan_src64(ROUTER).\ + filter_AMPLFMA().\ + filter(lambda p: p.ipv6inner.dst == MA2).\ + filter(lambda p: p.ipv6inner.hlim == HL_2).\ + filter(lambda p: p.icmpv6.echo.identifier == ECHO_ID4).\ + filter(lambda p: p.lowpan.frag).\ + must_next() + + # Step 10 + # - Device: BR_1 (DUT) + # - Description: Automatically forwards the ping request packet to the LAN. + # - Pass Criteria: + # - The DUT MUST forward the multicast ICMPv6 Echo (ping) Request packet to the LAN with the Hop Limit field set + # to 1. + print("Step 10: BR_1 forwards to LAN, HL=1") + pkts.filter_eth_src(vars['BR_1_ETH']).\ + filter_ping_request(identifier=ECHO_ID4).\ + filter(lambda p: p.ipv6.dst == MA2).\ + filter(lambda p: p.ipv6.hlim == HL_1).\ + must_next() + + # Step 11 + # - Device: Router + # - Description: Harness instructs the device to send an ICMPv6 Echo (ping) Request packet encapsulated in an MPL + # multicast packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet set + # to 1. The size of the payload is 130 bytes. + # - Pass Criteria: + # - N/A + print("Step 11: Router sends Echo Request in MPL to MA2, inner HL=1") + pkts.filter_wpan_src64(ROUTER).\ + filter_AMPLFMA().\ + filter(lambda p: p.ipv6inner.dst == MA2).\ + filter(lambda p: p.ipv6inner.hlim == HL_1).\ + filter(lambda p: p.icmpv6.echo.identifier == ECHO_ID5).\ + filter(lambda p: p.lowpan.frag).\ + must_next() + + # Step 12 + # - Device: BR_1 (DUT) + # - Description: Does not forward the ping request packet to the LAN. + # - Pass Criteria: + # - The DUT MUST NOT forward the ICMPv6 Echo (ping) Request packet to the LAN. + print("Step 12: BR_1 MUST NOT forward to LAN") + pkts.copy().\ + filter_eth_src(vars['BR_1_ETH']).\ + filter_ping_request(identifier=ECHO_ID5).\ + must_not_next() + + # Step 13 + # - Device: Router + # - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet encapsulated in an MPL + # multicast packet to the multicast address, MA2, with the Hop Limit field of the inner (encapsulated) packet set + # to 0. + # - Pass Criteria: + # - N/A + print("Step 13: Router sends Echo Request in MPL to MA2, inner HL=0") + pkts.filter_wpan_src64(ROUTER).\ + filter_AMPLFMA().\ + filter(lambda p: p.ipv6inner.dst == MA2).\ + filter(lambda p: p.ipv6inner.hlim == HL_0).\ + filter(lambda p: p.icmpv6.echo.identifier == ECHO_ID6).\ + filter(lambda p: p.lowpan.frag).\ + must_next() + + # Step 14 + # - Device: BR_1 (DUT) + # - Description: Does not forward the ping request packet to the LAN. + # - Pass Criteria: + # - The DUT MUST NOT forward the ICMPv6 Echo (ping) Request packet to the LAN. + print("Step 14: BR_1 MUST NOT forward to LAN") + pkts.copy().\ + filter_eth_src(vars['BR_1_ETH']).\ + filter_ping_request(identifier=ECHO_ID6).\ + must_not_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify)