[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.
This commit is contained in:
Jonathan Hui
2026-03-16 20:35:22 -05:00
committed by GitHub
parent 725b101150
commit 0f44bd990e
11 changed files with 910 additions and 22 deletions
+1
View File
@@ -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")
+46 -1
View File
@@ -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())
{
+7 -7
View File
@@ -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);
+3 -1
View File
@@ -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;
+29
View File
@@ -164,8 +164,17 @@ void Node::HandleReceive(otMessage *aMessage)
Core::Get().SetActiveNode(this);
if (header->GetDestination().IsMulticast())
{
VerifyOrExit(Get<BackboneRouter::Local>().IsPrimary());
}
VerifyOrExit(Get<NetworkData::Leader>().IsOnMesh(header->GetSource()));
// Only forward if source is NOT Link-Local and NOT Mesh-Local.
VerifyOrExit(!header->GetSource().IsLinkLocalUnicastOrMulticast());
VerifyOrExit(!Get<Mle::Mle>().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<ThreadNetif>().GetUnicastAddresses())
{
const Ip6::Address &address = unicastAddress.GetAddress();
if (address.GetScope() == Ip6::Address::kGlobalScope && !Get<Mle::Mle>().IsMeshLocalAddress(address))
{
matchedAddress = &address;
break;
}
}
VerifyOrQuit(matchedAddress != nullptr, "no global address found");
return *matchedAddress;
}
} // namespace Nexus
} // namespace ot
+10
View File
@@ -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(); }
+29 -8
View File
@@ -104,7 +104,7 @@ void Pcap::Open(const char *aFilename)
ClearAllBytes(idb);
idb.mBlockType = LittleEndian::HostSwap(kPcapngIdbType);
idb.mBlockTotalLength = LittleEndian::HostSwap(static_cast<uint32_t>(sizeof(idb)));
idb.mLinkType = LittleEndian::HostSwap(static_cast<uint16_t>(kPcapngLinkTypeIPv6));
idb.mLinkType = LittleEndian::HostSwap(static_cast<uint16_t>(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<uint32_t>(aTimeUs >> 32));
epb.mTimestampLow = LittleEndian::HostSwap(static_cast<uint32_t>(aTimeUs & 0xffffffff));
epb.mCapturedLen = LittleEndian::HostSwap(static_cast<uint32_t>(aLength));
epb.mOriginalLen = LittleEndian::HostSwap(static_cast<uint32_t>(aLength));
epb.mCapturedLen = LittleEndian::HostSwap(packetLen);
epb.mOriginalLen = LittleEndian::HostSwap(packetLen);
VerifyOrExit(fwrite(&epb, sizeof(epb), 1, mFile) == 1, Close());
VerifyOrExit(fwrite(&ethHeader, 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());
+14 -5
View File
@@ -32,6 +32,7 @@
#include <cstdint>
#include <cstdio>
#include <openthread/platform/infra_if.h>
#include <openthread/platform/radio.h>
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;
+1
View File
@@ -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
+479
View File
@@ -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 <stdio.h>
#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<Mle::Mle>().IsLeader());
br1.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
br1.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().SetEnabled(true));
br1.Get<BackboneRouter::Local>().SetEnabled(true);
br2.Join(br1, Node::kAsFtd);
router.Join(br1, Node::kAsFtd);
nexus.AdvanceTime(kAttachToRouterTime);
VerifyOrQuit(br2.Get<Mle::Mle>().IsRouter());
VerifyOrQuit(router.Get<Mle::Mle>().IsRouter());
br2.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
br2.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(br2.Get<BorderRouter::RoutingManager>().SetEnabled(true));
br2.Get<BackboneRouter::Local>().SetEnabled(true);
nexus.AdvanceTime(kStabilizationTime);
VerifyOrQuit(br1.Get<BackboneRouter::Local>().IsPrimary());
VerifyOrQuit(!br2.Get<BackboneRouter::Local>().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 <srcaddr>
* - Pass Criteria:
* - N/A
*/
router.SendEchoRequest(globalMcast, kEchoIdentifier, kEchoPayloadSize, 64,
&router.Get<Mle::Mle>().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 <srcaddr>
* - Pass Criteria:
* - N/A
*/
router.SendEchoRequest(globalMcast, kEchoIdentifier, kEchoPayloadSize, 64,
&router.Get<Mle::Mle>().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;
}
+291
View File
@@ -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)