From 0f44bd990ed25abaac1fa94fe12cdda9760d5566 Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Mon, 16 Mar 2026 20:35:22 -0500 Subject: [PATCH] [nexus] add MATN-TC-07 test case for BBR multicast forwarding (#12694) This commit implements the MATN-TC-07 test case in the Nexus simulation framework to verify default multicast forwarding behavior on Border Routers. Key implementation details include: - Implementation of the MATN-TC-07 test scenario in C++ to trigger various multicast ping requests across different IPv6 scopes (realm-local, admin-local, site-local, global, and link-local). - Enhancement of the Python verification script to strictly validate that only the Primary BBR forwards multicast packets to the backbone link using Ethernet source address filtering. - Support for Ethernet link type in Nexus PCAP generation by prepending Ethernet headers to infrastructure IPv6 packets. - Exposure of infrastructure MAC addresses (ethaddrs) in the test information JSON to enable identification of the forwarding node on the backbone link. - Support for verifying source addresses of MPL-encapsulated multicast packets by checking both outer and inner headers. - Addition of FindGlobalAddress() helper in the Nexus node platform. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/platform/nexus_core.cpp | 47 ++- tests/nexus/platform/nexus_infra_if.cpp | 14 +- tests/nexus/platform/nexus_infra_if.hpp | 4 +- tests/nexus/platform/nexus_node.cpp | 29 ++ tests/nexus/platform/nexus_node.hpp | 10 + tests/nexus/platform/nexus_pcap.cpp | 37 +- tests/nexus/platform/nexus_pcap.hpp | 19 +- tests/nexus/run_nexus_tests.sh | 1 + tests/nexus/test_1_2_MATN_TC_7.cpp | 479 ++++++++++++++++++++++++ tests/nexus/verify_1_2_MATN_TC_7.py | 291 ++++++++++++++ 11 files changed, 910 insertions(+), 22 deletions(-) create mode 100644 tests/nexus/test_1_2_MATN_TC_7.cpp create mode 100644 tests/nexus/verify_1_2_MATN_TC_7.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index e0145a72d..5310ebed5 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -239,6 +239,7 @@ ot_nexus_test(1_2_MATN_TC_2 "cert;nexus") ot_nexus_test(1_2_MATN_TC_3 "cert;nexus") ot_nexus_test(1_2_MATN_TC_4 "cert;nexus") ot_nexus_test(1_2_MATN_TC_5 "cert;nexus") +ot_nexus_test(1_2_MATN_TC_7 "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 028cc4c4c..a527e8f1f 100644 --- a/tests/nexus/platform/nexus_core.cpp +++ b/tests/nexus/platform/nexus_core.cpp @@ -168,6 +168,18 @@ void Core::SaveTestInfo(const char *aFilename, Node *aLeaderNode) } fprintf(file, " },\n"); + fprintf(file, " \"ethaddrs\": {\n"); + for (Node &node : mNodes) + { + 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, " },\n"); + fprintf(file, " \"rloc16s\": {\n"); for (Node &node : mNodes) { @@ -547,7 +559,40 @@ void Core::ProcessInfraIf(Node &aNode) VerifyOrQuit(message->GetLength() >= sizeof(Ip6::Header) && header.IsVersion6()); SuccessOrQuit(msgData.SetFrom(*message, 0, message->GetLength())); - mPcap.WritePacket(msgData.GetBytes(), msgData.GetLength(), mNow); + + { + InfraIf::LinkLayerAddress srcMac; + InfraIf::LinkLayerAddress dstMac; + + aNode.mInfraIf.GetLinkLayerAddress(srcMac); + + if (header.GetDestination().IsMulticast()) + { + dstMac.mLength = 6; + dstMac.mAddress[0] = 0x33; + dstMac.mAddress[1] = 0x33; + dstMac.mAddress[2] = header.GetDestination().mFields.m8[12]; + dstMac.mAddress[3] = header.GetDestination().mFields.m8[13]; + dstMac.mAddress[4] = header.GetDestination().mFields.m8[14]; + dstMac.mAddress[5] = header.GetDestination().mFields.m8[15]; + } + else + { + Node *dstNode = FindNodeByInfraIfAddress(header.GetDestination()); + + if (dstNode != nullptr) + { + dstNode->mInfraIf.GetLinkLayerAddress(dstMac); + } + else + { + dstMac.mLength = 6; + memset(dstMac.mAddress, 0xff, 6); + } + } + + mPcap.WritePacket(srcMac, dstMac, msgData.GetBytes(), msgData.GetLength(), mNow); + } if (!header.GetDestination().IsMulticast()) { diff --git a/tests/nexus/platform/nexus_infra_if.cpp b/tests/nexus/platform/nexus_infra_if.cpp index 0c1b65721..3943b10eb 100644 --- a/tests/nexus/platform/nexus_infra_if.cpp +++ b/tests/nexus/platform/nexus_infra_if.cpp @@ -43,9 +43,9 @@ InfraIf::InfraIf(void) void InfraIf::Init(Node &aNode) { - Ip6::Address address; - otPlatInfraIfLinkLayerAddress mac; - Ip6::InterfaceIdentifier iid; + Ip6::Address address; + LinkLayerAddress mac; + Ip6::InterfaceIdentifier iid; mIfIndex = 1; mNode = &aNode; @@ -188,9 +188,9 @@ exit: void InfraIf::HandlePrefixInfoOption(const Ip6::Nd::PrefixInfoOption &aPio) { - Ip6::Prefix prefix; - Ip6::Address address; - otPlatInfraIfLinkLayerAddress mac; + Ip6::Prefix prefix; + Ip6::Address address; + LinkLayerAddress mac; VerifyOrExit(aPio.IsAutoAddrConfigFlagSet()); @@ -437,7 +437,7 @@ void InfraIf::HandleEchoRequest(const Ip6::Header &aHeader, Message &aMessage) mPendingTxQueue.Enqueue(*replyMessage); } -void InfraIf::GetLinkLayerAddress(otPlatInfraIfLinkLayerAddress &aLinkLayerAddress) const +void InfraIf::GetLinkLayerAddress(LinkLayerAddress &aLinkLayerAddress) const { // Use a unique MAC address based on Node ID ClearAllBytes(aLinkLayerAddress); diff --git a/tests/nexus/platform/nexus_infra_if.hpp b/tests/nexus/platform/nexus_infra_if.hpp index 62d63eb5a..368ae457d 100644 --- a/tests/nexus/platform/nexus_infra_if.hpp +++ b/tests/nexus/platform/nexus_infra_if.hpp @@ -40,6 +40,8 @@ class Node; class InfraIf { public: + typedef otPlatInfraIfLinkLayerAddress LinkLayerAddress; ///< A link-layer address + InfraIf(void); void Init(Node &aNode); @@ -70,7 +72,7 @@ public: uint16_t aDestPort, uint16_t aPayloadSize); void Receive(Node &aSrcNode, const Ip6::Header &aHeader, Message &aMessage); - void GetLinkLayerAddress(otPlatInfraIfLinkLayerAddress &aLinkLayerAddress) const; + void GetLinkLayerAddress(LinkLayerAddress &aLinkLayerAddress) const; Node &GetNode(void); const Node &GetNode(void) const; diff --git a/tests/nexus/platform/nexus_node.cpp b/tests/nexus/platform/nexus_node.cpp index f2e4a5f97..3181d8c65 100644 --- a/tests/nexus/platform/nexus_node.cpp +++ b/tests/nexus/platform/nexus_node.cpp @@ -164,8 +164,17 @@ void Node::HandleReceive(otMessage *aMessage) Core::Get().SetActiveNode(this); + if (header->GetDestination().IsMulticast()) + { + VerifyOrExit(Get().IsPrimary()); + } + VerifyOrExit(Get().IsOnMesh(header->GetSource())); + // Only forward if source is NOT Link-Local and NOT Mesh-Local. + VerifyOrExit(!header->GetSource().IsLinkLocalUnicastOrMulticast()); + VerifyOrExit(!Get().IsMeshLocalAddress(header->GetSource())); + mInfraIf.SendIp6(header->GetSource(), header->GetDestination(), buffer, length); exit: @@ -201,5 +210,25 @@ const Ip6::Address &Node::FindMatchingAddress(const char *aPrefix) return *matchedAddress; } +const Ip6::Address &Node::FindGlobalAddress(void) +{ + const Ip6::Address *matchedAddress = nullptr; + + for (const Ip6::Netif::UnicastAddress &unicastAddress : Get().GetUnicastAddresses()) + { + const Ip6::Address &address = unicastAddress.GetAddress(); + + if (address.GetScope() == Ip6::Address::kGlobalScope && !Get().IsMeshLocalAddress(address)) + { + matchedAddress = &address; + break; + } + } + + VerifyOrQuit(matchedAddress != nullptr, "no global address found"); + + return *matchedAddress; +} + } // namespace Nexus } // namespace ot diff --git a/tests/nexus/platform/nexus_node.hpp b/tests/nexus/platform/nexus_node.hpp index 21b0fee16..5f36e1bbb 100644 --- a/tests/nexus/platform/nexus_node.hpp +++ b/tests/nexus/platform/nexus_node.hpp @@ -104,6 +104,16 @@ public: // a test failure (emits error message and exits the program.) const Ip6::Address &FindMatchingAddress(const char *aPrefix); + /** + * Finds and returns a global scope address on the device. + * + * It requires a global scope address to be found, otherwise it is treated as a test failure (emits error message + * and exits the program). + * + * @returns A reference to the global scope address. + */ + const Ip6::Address &FindGlobalAddress(void); + void SetName(const char *aName) { mName.Clear().Append("%s", aName); } void SetName(const char *aPrefix, uint16_t aIndex); const char *GetName(void) const { return mName.AsCString(); } diff --git a/tests/nexus/platform/nexus_pcap.cpp b/tests/nexus/platform/nexus_pcap.cpp index 62181455f..b08a3f59d 100644 --- a/tests/nexus/platform/nexus_pcap.cpp +++ b/tests/nexus/platform/nexus_pcap.cpp @@ -104,7 +104,7 @@ void Pcap::Open(const char *aFilename) ClearAllBytes(idb); idb.mBlockType = LittleEndian::HostSwap(kPcapngIdbType); idb.mBlockTotalLength = LittleEndian::HostSwap(static_cast(sizeof(idb))); - idb.mLinkType = LittleEndian::HostSwap(static_cast(kPcapngLinkTypeIPv6)); + idb.mLinkType = LittleEndian::HostSwap(static_cast(kPcapngLinkTypeEthernet)); idb.mSnapLen = LittleEndian::HostSwap(kPcapngSnapLen); idb.mBlockTotalLength2 = idb.mBlockTotalLength; VerifyOrExit(fwrite(&idb, sizeof(idb), 1, mFile) == 1, Close()); @@ -201,34 +201,55 @@ exit: return; } -void Pcap::WritePacket(const uint8_t *aBuffer, uint16_t aLength, uint64_t aTimeUs) +void Pcap::WritePacket(const otPlatInfraIfLinkLayerAddress &aSrcAddr, + const otPlatInfraIfLinkLayerAddress &aDstAddr, + const uint8_t *aBuffer, + uint16_t aLength, + uint64_t aTimeUs) { Epb epb; + OT_TOOL_PACKED_BEGIN + struct EthernetHeader + { + uint8_t mDst[6]; + uint8_t mSrc[6]; + uint16_t mType; + } OT_TOOL_PACKED_END; + EthernetHeader ethHeader; + + uint32_t packetLen; uint32_t paddedLen; uint32_t blockTotalLength; uint32_t padding = 0; VerifyOrExit(mFile != nullptr); - paddedLen = (aLength + 3) & ~3u; + ClearAllBytes(ethHeader); + memcpy(ethHeader.mDst, aDstAddr.mAddress, 6); + memcpy(ethHeader.mSrc, aSrcAddr.mAddress, 6); + ethHeader.mType = BigEndian::HostSwap16(kEtherTypeIPv6); + + packetLen = sizeof(ethHeader) + aLength; + paddedLen = (packetLen + 3) & ~3u; blockTotalLength = sizeof(epb) + paddedLen + sizeof(uint32_t); ClearAllBytes(epb); epb.mBlockType = LittleEndian::HostSwap(kPcapngEpbType); epb.mBlockTotalLength = LittleEndian::HostSwap(blockTotalLength); - epb.mInterfaceId = LittleEndian::HostSwap(1u); // Use Interface ID 1 for IPv6 + epb.mInterfaceId = LittleEndian::HostSwap(1u); // Use Interface ID 1 for Ethernet epb.mTimestampHigh = LittleEndian::HostSwap(static_cast(aTimeUs >> 32)); epb.mTimestampLow = LittleEndian::HostSwap(static_cast(aTimeUs & 0xffffffff)); - epb.mCapturedLen = LittleEndian::HostSwap(static_cast(aLength)); - epb.mOriginalLen = LittleEndian::HostSwap(static_cast(aLength)); + epb.mCapturedLen = LittleEndian::HostSwap(packetLen); + epb.mOriginalLen = LittleEndian::HostSwap(packetLen); VerifyOrExit(fwrite(&epb, sizeof(epb), 1, mFile) == 1, Close()); + VerifyOrExit(fwrite(ðHeader, sizeof(ethHeader), 1, mFile) == 1, Close()); VerifyOrExit(fwrite(aBuffer, aLength, 1, mFile) == 1, Close()); - if (paddedLen > aLength) + if (paddedLen > packetLen) { - VerifyOrExit(fwrite(&padding, paddedLen - aLength, 1, mFile) == 1, Close()); + VerifyOrExit(fwrite(&padding, paddedLen - packetLen, 1, mFile) == 1, Close()); } VerifyOrExit(fwrite(&epb.mBlockTotalLength, sizeof(epb.mBlockTotalLength), 1, mFile) == 1, Close()); diff --git a/tests/nexus/platform/nexus_pcap.hpp b/tests/nexus/platform/nexus_pcap.hpp index b26f700f8..8ee6b8f83 100644 --- a/tests/nexus/platform/nexus_pcap.hpp +++ b/tests/nexus/platform/nexus_pcap.hpp @@ -32,6 +32,7 @@ #include #include +#include #include namespace ot { @@ -66,11 +67,17 @@ public: /** * Writes a packet to the pcapng file. * - * @param[in] aBuffer The packet buffer to write. - * @param[in] aLength The packet length. - * @param[in] aTimeUs The timestamp in microseconds. + * @param[in] aSrcAddr The source link-layer address. + * @param[in] aDstAddr The destination link-layer address. + * @param[in] aBuffer The packet buffer to write. + * @param[in] aLength The packet length. + * @param[in] aTimeUs The timestamp in microseconds. */ - void WritePacket(const uint8_t *aBuffer, uint16_t aLength, uint64_t aTimeUs); + void WritePacket(const otPlatInfraIfLinkLayerAddress &aSrcAddr, + const otPlatInfraIfLinkLayerAddress &aDstAddr, + const uint8_t *aBuffer, + uint16_t aLength, + uint64_t aTimeUs); private: static constexpr uint32_t kPcapngShbType = 0x0a0d0d0a; @@ -81,7 +88,9 @@ private: static constexpr uint32_t kPcapngEpbType = 0x00000006; static constexpr uint32_t kPcapngSnapLen = 65535; static constexpr uint32_t kPcapngLinkTypeIeee802154 = 283; // DLT_IEEE802_15_4_TAP - static constexpr uint32_t kPcapngLinkTypeIPv6 = 101; // LINKTYPE_RAW + static constexpr uint32_t kPcapngLinkTypeEthernet = 1; // LINKTYPE_ETHERNET + + static constexpr uint16_t kEtherTypeIPv6 = 0x86dd; static constexpr uint8_t kTapVersion = 0; diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index cad71571d..1eb59345c 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -174,6 +174,7 @@ DEFAULT_TESTS=( "1_2_MATN_TC_3" "1_2_MATN_TC_4" "1_2_MATN_TC_5" + "1_2_MATN_TC_7" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_1_2_MATN_TC_7.cpp b/tests/nexus/test_1_2_MATN_TC_7.cpp new file mode 100644 index 000000000..9c22b0d44 --- /dev/null +++ b/tests/nexus/test_1_2_MATN_TC_7.cpp @@ -0,0 +1,479 @@ +/* + * 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 identifier. + */ +static constexpr uint16_t kEchoIdentifier = 0x1234; + +/** + * ICMPv6 Echo Request payload size. + */ +static constexpr uint16_t kEchoPayloadSize = 10; + +/** + * Infrastructure interface index. + */ +static constexpr uint32_t kInfraIfIndex = 1; + +/** + * Multicast address MA5 (realm-local). + */ +static const char kMA5[] = "ff03::1234:777a:1"; + +/** + * Multicast address (admin-local). + */ +static const char kAdminLocalMcast[] = "ff04::1234:777a:1"; + +/** + * Multicast address (site-local). + */ +static const char kSiteLocalMcast[] = "ff05::1234:777a:1"; + +/** + * Multicast address (global). + */ +static const char kGlobalMcast[] = "ff0e::1234:777a:1"; + +/** + * Multicast address (link-local). + */ +static const char kLinkLocalMcast[] = "ff02::1234:777a:1"; + +void TestMatnTc7(void) +{ + /** + * 5.10.6 MATN-TC-07: Default BR multicast forwarding + * + * 5.10.6.1 Topology + * - BR_1 + * - BR_2 + * - ROUTER + * + * 5.10.6.2 Purpose & Description + * Verify if the default forwarding of multicast on a Primary BBR is correct. Note: this can be changed by + * forwarding flags of the BBR; however, this is application specific and change is not tested. This test case + * also verifies that a Secondary BBR does not forward multicast packets to the backbone link. + * + * Spec Reference | V1.2 Section | V1.3.0 Section + * -----------------|--------------|--------------- + * Multicast | 5.10.6 | N/A + */ + + Core nexus; + Node &br1 = nexus.CreateNode(); + Node &br2 = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + + Ip6::Address ma5; + Ip6::Address adminLocalMcast; + Ip6::Address siteLocalMcast; + Ip6::Address globalMcast; + Ip6::Address linkLocalMcast; + + br1.SetName("BR_1"); + br2.SetName("BR_2"); + router.SetName("ROUTER"); + + SuccessOrQuit(ma5.FromString(kMA5)); + SuccessOrQuit(adminLocalMcast.FromString(kAdminLocalMcast)); + SuccessOrQuit(siteLocalMcast.FromString(kSiteLocalMcast)); + SuccessOrQuit(globalMcast.FromString(kGlobalMcast)); + SuccessOrQuit(linkLocalMcast.FromString(kLinkLocalMcast)); + + nexus.AdvanceTime(0); + + Instance::SetLogLevel(kLogLevelNote); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 0: Topology formation - BR_1, BR_2, ROUTER"); + + /** + * Step 0 + * - Device: N/A + * - Description: Topology formation - BR_1, BR_2, ROUTER + * - Pass Criteria: + * - N/A + */ + + br1.AllowList(br2); + br1.AllowList(router); + br2.AllowList(br1); + br2.AllowList(router); + router.AllowList(br1); + router.AllowList(br2); + + br1.Form(); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(br1.Get().IsLeader()); + + br1.Get().Init(kInfraIfIndex, true); + br1.Get().Init(); + SuccessOrQuit(br1.Get().SetEnabled(true)); + br1.Get().SetEnabled(true); + + br2.Join(br1, Node::kAsFtd); + router.Join(br1, Node::kAsFtd); + nexus.AdvanceTime(kAttachToRouterTime); + + VerifyOrQuit(br2.Get().IsRouter()); + VerifyOrQuit(router.Get().IsRouter()); + + br2.Get().Init(kInfraIfIndex, true); + br2.Get().Init(); + SuccessOrQuit(br2.Get().SetEnabled(true)); + br2.Get().SetEnabled(true); + + nexus.AdvanceTime(kStabilizationTime); + + VerifyOrQuit(br1.Get().IsPrimary()); + VerifyOrQuit(!br2.Get().IsPrimary()); + + const Ip6::Address &routerGlobalAddr = router.FindGlobalAddress(); + + nexus.AddTestVar("MA5", kMA5); + nexus.AddTestVar("ADMIN_LOCAL_MCAST", kAdminLocalMcast); + nexus.AddTestVar("SITE_LOCAL_MCAST", kSiteLocalMcast); + nexus.AddTestVar("GLOBAL_MCAST", kGlobalMcast); + nexus.AddTestVar("LINK_LOCAL_MCAST", kLinkLocalMcast); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: ROUTER sends an ICMPv6 Echo (ping) Request packet with realm-local scope MA5."); + + /** + * Step 1 + * - Device: ROUTER + * - Description: Harness instructs the device to send an ICMPv6 Echo (ping) Request packet with realm-local scope + * MA5. + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(ma5, kEchoIdentifier, kEchoPayloadSize, 64, &routerGlobalAddr); + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: BR_1 does not forward the multicast ping request packet to the LAN."); + + /** + * Step 2 + * - Device: BR_1 + * - Description: Does not forward the multicast ping request packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_1: + * - The DUT MUST NOT forward the multicast ICMPv6 Echo (ping) Request packet with realm-local scope address + * MA5 to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: BR_2 does not forward the multicast ping request packet to the LAN."); + + /** + * Step 3 + * - Device: BR_2 + * - Description: Does not forward the multicast ping request packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_2: + * - The DUT MUST NOT forward the multicast ICMPv6 Echo (ping) Request packet with realm-local scope address + * MA5 to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: ROUTER sends an ICMPv6 Echo Request with admin-local scope, encapsulated in an MPL packet."); + + /** + * Step 4 + * - Device: ROUTER + * - Description: Harness instructs the device to sends an ICMPv6 Echo (ping) Request packet with admin-local scope + * (address ff04::…), encapsulated in an MPL packet. + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(adminLocalMcast, kEchoIdentifier, kEchoPayloadSize, 64, &routerGlobalAddr); + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: BR_1 automatically forwards the ping request packet to the LAN."); + + /** + * Step 5 + * - Device: BR_1 + * - Description: Automatically forwards the ping request packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_1: + * - The DUT MUST forward the multicast ICMPv6 Echo (ping) Request packet with admin-local scope (address + * ff04::…) to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: BR_2 does not forward the multicast ping request packet to the LAN."); + + /** + * Step 6 + * - Device: BR_2 + * - Description: Does not forward the multicast ping request packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_2: + * - The DUT MUST NOT forward the multicast ICMPv6 Echo (ping) Request packet with admin-local scope (address + * ff04::…) to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 7: ROUTER sends an ICMPv6 Echo Request with site-local scope, encapsulated in an MPL packet."); + + /** + * Step 7 + * - Device: ROUTER + * - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet with site-local scope + * (address ff05::…), encapsulated in an MPL packet. + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(siteLocalMcast, kEchoIdentifier, kEchoPayloadSize, 64, &routerGlobalAddr); + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 8: BR_1 automatically forwards the ping request packet to the LAN."); + + /** + * Step 8 + * - Device: BR_1 + * - Description: Automatically forwards the ping request packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_1: + * - The DUT MUST forward the multicast ICMPv6 Echo (ping) Request packet with site-local scope (address + * ff05::…) to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 9: BR_2 does not forward the multicast ping request packet to the LAN."); + + /** + * Step 9 + * - Device: BR_2 + * - Description: Does not forward the multicast ping request packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_2: + * - The DUT MUST NOT forward the multicast ICMPv6 Echo (ping) Request packet with site-local scope (address + * ff05::…) to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 10: ROUTER sends an ICMPv6 Echo Request with global scope, encapsulated in an MPL packet."); + + /** + * Step 10 + * - Device: ROUTER + * - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet with global scope + * (address ff0e::…) , encapsulated in an MPL packet. + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(globalMcast, kEchoIdentifier, kEchoPayloadSize, 64, &routerGlobalAddr); + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 11: BR_1 automatically forwards the ping request packet to the LAN."); + + /** + * Step 11 + * - Device: BR_1 + * - Description: Automatically forwards the ping request packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_1: + * - The DUT MUST forward the multicast ICMPv6 Echo (ping) Request packet with global scope (address ff0e::…) + * to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 12: BR_2 does not forward the multicast ping request packet to the LAN."); + + /** + * Step 12 + * - Device: BR_2 + * - Description: Does not forward the multicast ping request packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_2: + * - The DUT MUST NOT forward the multicast ICMPv6 Echo (ping) Request packet with global scope (address + * ff0e::…) to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 13: ROUTER sends an ICMPv6 Echo (ping) Request packet with link-local scope."); + + /** + * Step 13 + * - Device: ROUTER + * - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet with link-local scope + * (address ff02::…). + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(linkLocalMcast, kEchoIdentifier, kEchoPayloadSize, 64, &routerGlobalAddr); + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 14: BR_1 does not forward the multicast ping packet to the LAN."); + + /** + * Step 14 + * - Device: BR_1 + * - Description: Does not forward the multicast ping packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_1: + * - The DUT MUST NOT forward the multicast ping packet with link-local scope (address ff02:…) to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 15: BR_2 does not forward the multicast ping packet to the LAN."); + + /** + * Step 15 + * - Device: BR_2 + * - Description: Does not forward the multicast ping packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_2: + * - The DUT MUST NOT forward the multicast ping packet with link-local scope (address ff02:…) to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 16: ROUTER sends an ICMPv6 Echo Request with global scope and ML-EID as source."); + + /** + * Step 16 + * - Device: ROUTER + * - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request with global scope (address + * ff0e::…) , encapsulated in an MPL packet. The source address is chosen as the ML-EID. This implies that the + * packet has to stay on the mesh. Note: this can be implemented in OT CLI using ping -I + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(globalMcast, kEchoIdentifier, kEchoPayloadSize, 64, + &router.Get().GetMeshLocalEid()); + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 17: BR_1 does not forward the multicast ping packet to the LAN."); + + /** + * Step 17 + * - Device: BR_1 + * - Description: Does not forward the multicast ping packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_1: + * - The DUT MUST NOT forward the multicast ping packet with global scope to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 18: BR_2 does not forward the multicast ping packet to the LAN."); + + /** + * Step 18 + * - Device: BR_2 + * - Description: Does not forward the multicast ping packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_2: + * - The DUT MUST NOT forward the multicast ping packet with global scope to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 19: ROUTER sends an ICMPv6 Echo Request with global scope and link-local address as source."); + + /** + * Step 19 + * - Device: ROUTER + * - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request with global scope (address + * ff0e::…) , encapsulated in an MPL packet. The source address is chosen as the link-local address (fe80::...). + * This implies that the encapsulated packet must never be forwarded. Note: this can be implemented in OT CLI + * using ping -I + * - Pass Criteria: + * - N/A + */ + router.SendEchoRequest(globalMcast, kEchoIdentifier, kEchoPayloadSize, 64, + &router.Get().GetLinkLocalAddress()); + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 20: BR_1 does not forward the multicast ping packet to the LAN."); + + /** + * Step 20 + * - Device: BR_1 + * - Description: Does not forward the multicast ping packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_1: + * - The DUT MUST NOT forward the multicast ping packet with global scope to the LAN. + */ + + Log("---------------------------------------------------------------------------------------"); + Log("Step 21: BR_2 does not forward the multicast ping packet to the LAN."); + + /** + * Step 21 + * - Device: BR_2 + * - Description: Does not forward the multicast ping packet to the LAN. + * - Pass Criteria: + * - For DUT = BR_2: + * - The DUT MUST NOT forward the multicast ping packet with global scope to the LAN. + */ + + nexus.SaveTestInfo("test_1_2_MATN_TC_7.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestMatnTc7(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_1_2_MATN_TC_7.py b/tests/nexus/verify_1_2_MATN_TC_7.py new file mode 100644 index 000000000..3be47e7a5 --- /dev/null +++ b/tests/nexus/verify_1_2_MATN_TC_7.py @@ -0,0 +1,291 @@ +#!/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.addrs import Ipv6Addr + + +def verify(pv): + # 5.10.6 MATN-TC-07: Default BR multicast forwarding + # + # 5.10.6.1 Topology + # - BR_1 + # - BR_2 + # - ROUTER + # + # 5.10.6.2 Purpose & Description + # Verify if the default forwarding of multicast on a Primary BBR is correct. Note: this can be changed by + # forwarding flags of the BBR; however, this is application specific and change is not tested. This test case + # also verifies that a Secondary BBR does not forward multicast packets to the backbone link. + # + # Spec Reference | V1.2 Section | V1.3.0 Section + # -----------------|--------------|--------------- + # Multicast | 5.10.6 | N/A + + pkts = pv.pkts + vars = pv.vars + + # Step 0 + # - Device: N/A + # - Description: Topology formation - BR_1, BR_2, ROUTER + # - Pass Criteria: + # - N/A + print("Step 0: Topology formation - BR_1, BR_2, ROUTER") + + # Step 1 + # - Device: ROUTER + # - Description: Harness instructs the device to send an ICMPv6 Echo (ping) Request packet with realm-local scope + # MA5. + # - Pass Criteria: + # - N/A + print("Step 1: ROUTER sends an ICMPv6 Echo (ping) Request packet with realm-local scope MA5.") + pkts.filter_ping_request().\ + filter(lambda p: p.wpan).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['MA5']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_next() + + # Step 2 & 3 + # - Device: BR_1, BR_2 + # - Description: Does not forward the multicast ping request packet to the LAN. + # - Pass Criteria: + # - The DUT MUST NOT forward the multicast ICMPv6 Echo (ping) Request packet with realm-local scope address + # MA5 to the LAN. + print("Step 2 & 3: BR_1 and BR_2 do not forward the multicast ping request packet to the LAN.") + pkts.copy().\ + filter_ping_request().\ + filter(lambda p: not p.wpan).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['MA5']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_not_next() + + # Step 4 + # - Device: ROUTER + # - Description: Harness instructs the device to sends an ICMPv6 Echo (ping) Request packet with admin-local scope + # (address ff04::…), encapsulated in an MPL packet. + # - Pass Criteria: + # - N/A + print("Step 4: ROUTER sends an ICMPv6 Echo Request with admin-local scope, encapsulated in an MPL packet.") + pkts_copy = pkts.copy() + pkts_copy.filter_ping_request().\ + filter(lambda p: p.wpan).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['ADMIN_LOCAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_next() + + # Step 5 & 6 + # - Device: BR_1, BR_2 + # - Description: BR_1 automatically forwards the ping request packet to the LAN. BR_2 does not. + # - Pass Criteria: + # - BR_1 MUST forward the multicast ICMPv6 Echo (ping) Request packet with admin-local scope to the LAN. + # - BR_2 MUST NOT forward it. + print("Step 5 & 6: BR_1 forwards the ping request to the LAN; BR_2 does not.") + # Check that it is forwarded to the LAN by BR_1. + # Note: MPL retransmissions may cause multiple copies on the LAN from the same BR. + pkts.filter_ping_request().\ + filter(lambda p: not p.wpan).\ + filter(lambda p: p.eth.src == vars['BR_1_ETH']).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['ADMIN_LOCAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_next() + + # Check that BR_2 does NOT forward it. + pkts.copy().\ + filter_ping_request().\ + filter(lambda p: not p.wpan).\ + filter(lambda p: p.eth.src == vars['BR_2_ETH']).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['ADMIN_LOCAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_not_next() + + if pkts.index < pkts_copy.index: + pkts.index = pkts_copy.index + + # Step 7 + # - Device: ROUTER + # - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet with site-local scope + # (address ff05::…), encapsulated in an MPL packet. + # - Pass Criteria: + # - N/A + print("Step 7: ROUTER sends an ICMPv6 Echo Request with site-local scope, encapsulated in an MPL packet.") + pkts_copy = pkts.copy() + pkts_copy.filter_ping_request().\ + filter(lambda p: p.wpan).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['SITE_LOCAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_next() + + # Step 8 & 9 + # - Device: BR_1, BR_2 + # - Description: BR_1 automatically forwards the ping request packet to the LAN. BR_2 does not. + print("Step 8 & 9: BR_1 forwards the ping request to the LAN; BR_2 does not.") + # Check that it is forwarded to the LAN by BR_1. + pkts.filter_ping_request().\ + filter(lambda p: not p.wpan).\ + filter(lambda p: p.eth.src == vars['BR_1_ETH']).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['SITE_LOCAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_next() + + # Check that BR_2 does NOT forward it. + pkts.copy().\ + filter_ping_request().\ + filter(lambda p: not p.wpan).\ + filter(lambda p: p.eth.src == vars['BR_2_ETH']).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['SITE_LOCAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_not_next() + + if pkts.index < pkts_copy.index: + pkts.index = pkts_copy.index + + # Step 10 + # - Device: ROUTER + # - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet with global scope + # (address ff0e::…) , encapsulated in an MPL packet. + # - Pass Criteria: + # - N/A + print("Step 10: ROUTER sends an ICMPv6 Echo Request with global scope, encapsulated in an MPL packet.") + pkts_copy = pkts.copy() + pkts_copy.filter_ping_request().\ + filter(lambda p: p.wpan).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['GLOBAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_next() + + # Step 11 & 12 + # - Device: BR_1, BR_2 + # - Description: BR_1 automatically forwards the ping request packet to the LAN. BR_2 does not. + print("Step 11 & 12: BR_1 forwards the ping request to the LAN; BR_2 does not.") + # Check that it is forwarded to the LAN by BR_1. + pkts.filter_ping_request().\ + filter(lambda p: not p.wpan).\ + filter(lambda p: p.eth.src == vars['BR_1_ETH']).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['GLOBAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_next() + + # Check that BR_2 does NOT forward it. + pkts.copy().\ + filter_ping_request().\ + filter(lambda p: not p.wpan).\ + filter(lambda p: p.eth.src == vars['BR_2_ETH']).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['GLOBAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_not_next() + + if pkts.index < pkts_copy.index: + pkts.index = pkts_copy.index + + # Step 13 + # - Device: ROUTER + # - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request packet with link-local scope + # (address ff02::…). + # - Pass Criteria: + # - N/A + print("Step 13: ROUTER sends an ICMPv6 Echo (ping) Request packet with link-local scope.") + pkts.filter_ping_request().\ + filter(lambda p: p.wpan).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['LINK_LOCAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_next() + + # Step 14 & 15 + # - Device: BR_1, BR_2 + # - Description: Does not forward the multicast ping packet to the LAN. + print("Step 14 & 15: BR_1 and BR_2 do not forward the multicast ping packet to the LAN.") + pkts.copy().\ + filter_ping_request().\ + filter(lambda p: not p.wpan).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['LINK_LOCAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_not_next() + + # Step 16 + # - Device: ROUTER + # - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request with global scope (address + # ff0e::…) , encapsulated in an MPL packet. The source address is chosen as the ML-EID. + # - Pass Criteria: + # - N/A + print("Step 16: ROUTER sends an ICMPv6 Echo Request with global scope and ML-EID as source.") + pkts.filter_ping_request().\ + filter(lambda p: p.wpan).\ + filter(lambda p: p.ipv6.src == vars['ROUTER_MLEID'] or p.ipv6inner.src == vars['ROUTER_MLEID']).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['GLOBAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_next() + + # Step 17 & 18 + # - Device: BR_1, BR_2 + # - Description: Does not forward the multicast ping packet to the LAN. + print("Step 17 & 18: BR_1 and BR_2 do not forward the multicast ping packet to the LAN.") + pkts.copy().\ + filter_ping_request().\ + filter(lambda p: not p.wpan).\ + filter(lambda p: p.ipv6.src == vars['ROUTER_MLEID'] or p.ipv6inner.src == vars['ROUTER_MLEID']).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['GLOBAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_not_next() + + # Step 19 + # - Device: ROUTER + # - Description: Harness instructs the device to send a ICMPv6 Echo (ping) Request with global scope (address + # ff0e::…) , encapsulated in an MPL packet. The source address is chosen as the link-local address (fe80::...). + # - Pass Criteria: + # - N/A + print("Step 19: ROUTER sends an ICMPv6 Echo Request with global scope and link-local address as source.") + pkts.filter_ping_request().\ + filter(lambda p: p.wpan).\ + filter(lambda p: p.ipv6.src == vars['ROUTER_LLA'] or p.ipv6inner.src == vars['ROUTER_LLA']).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['GLOBAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_next() + + # Step 20 & 21 + # - Device: BR_1, BR_2 + # - Description: Does not forward the multicast ping packet to the LAN. + print("Step 20 & 21: BR_1 and BR_2 do not forward the multicast ping packet to the LAN.") + pkts.copy().\ + filter_ping_request().\ + filter(lambda p: not p.wpan).\ + filter(lambda p: p.ipv6.src == vars['ROUTER_LLA'] or p.ipv6inner.src == vars['ROUTER_LLA']).\ + filter(lambda p: p.ipv6.dst == Ipv6Addr(vars['GLOBAL_MCAST']) or\ + p.ipv6.dst == 'ff03::fc').\ + must_not_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify)