diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index ff513e0c6..8a8f73b40 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -244,6 +244,7 @@ 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") ot_nexus_test(1_2_MATN_TC_15 "cert;nexus") +ot_nexus_test(1_2_MATN_TC_16 "cert;nexus") # Misc tests ot_nexus_test(border_admitter "core;nexus") diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index 9e981d77d..e03fc840e 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -75,7 +75,8 @@ #define OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE 1 #define OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE 1 #define OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE 1 -#define OPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS 4 +#define OPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS 80 +#define OPENTHREAD_CONFIG_MAX_MULTICAST_LISTENERS 80 #define OPENTHREAD_CONFIG_IP6_MAX_EXT_UCAST_ADDRS 8 #define OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE 1 #define OPENTHREAD_CONFIG_IP6_SLAAC_NUM_ADDRESSES 4 diff --git a/tests/nexus/platform/nexus_core.hpp b/tests/nexus/platform/nexus_core.hpp index fe55e320a..ff708f586 100644 --- a/tests/nexus/platform/nexus_core.hpp +++ b/tests/nexus/platform/nexus_core.hpp @@ -129,7 +129,7 @@ private: OwningList mNodes; Pcap mPcap; Array mNetworkKeys; - Array mTestVars; + Array mTestVars; uint16_t mCurNodeId; bool mPendingAction; uint64_t mNow; diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index 0eadbb364..55d008a47 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -179,6 +179,7 @@ DEFAULT_TESTS=( "1_2_MATN_TC_10" "1_2_MATN_TC_12" "1_2_MATN_TC_15" + "1_2_MATN_TC_16" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_1_2_MATN_TC_16.cpp b/tests/nexus/test_1_2_MATN_TC_16.cpp new file mode 100644 index 000000000..930d35714 --- /dev/null +++ b/tests/nexus/test_1_2_MATN_TC_16.cpp @@ -0,0 +1,306 @@ +/* + * 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; + +/** + * Time to advance for MLR registration, in milliseconds. + */ +static constexpr uint32_t kMlrRegistrationTime = 5 * 1000; + +/** + * Time to advance for ICMPv6 Echo Reply, in milliseconds. + */ +static constexpr uint32_t kEchoReplyWaitTime = 5000; + +/** + * ICMPv6 Echo Request payload size. + */ +static constexpr uint16_t kEchoPayloadSize = 10; + +/** + * ICMPv6 Echo Request identifier. + */ +static constexpr uint16_t kEchoIdentifier = 0x1234; + +/** + * Number of batches of multicast registrations. + */ +static constexpr uint8_t kNumBatches = 5; + +/** + * Number of multicast addresses per registration request. + */ +static constexpr uint8_t kNumAddressesPerBatch = 15; + +/** + * Total number of multicast addresses to subscribe. + */ +static constexpr uint8_t kTotalNumAddresses = kNumBatches * kNumAddressesPerBatch; + +/** + * Multicast address MA2 which is not registered. + */ +static const char kMA2[] = "ff05::1234:777a:1"; + +void TestMatnTc16(void) +{ + /** + * 5.10.12 MATN-TC-16: Large number of multicast group subscriptions to BBR + * + * 5.10.12.1 Topology + * - BR_1 (DUT) + * - Router + * - Host + * + * 5.10.12.2 Purpose & Description + * The purpose of this test case is to verify that the Primary BBR can handle a large number (75) of subscriptions + * to different multicast groups. Multicast registrations are performed each time with 15 multicast addresses + * per registration request. + * + * Spec Reference | V1.3.0 Section + * ---------------|--------------- + * Multicast | 5.10.12 + */ + + Core nexus; + Node &br1 = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + Node &host = nexus.CreateNode(); + Ip6::Address mas[kTotalNumAddresses]; + Ip6::Address ma2; + const Ip6::Address *hostUla; + + br1.SetName("BR_1"); + router.SetName("ROUTER"); + host.SetName("HOST"); + + for (uint8_t i = 0; i < kTotalNumAddresses; i++) + { + char buf[OT_IP6_ADDRESS_STRING_SIZE]; + snprintf(buf, sizeof(buf), "ff04::1234:777a:%x", i + 1); + SuccessOrQuit(mas[i].FromString(buf)); + } + + SuccessOrQuit(ma2.FromString(kMA2)); + + nexus.AdvanceTime(0); + + Instance::SetLogLevel(kLogLevelNote); + + /** + * Step 0 + * - Device: N/A + * - Description: Topology formation – BR_1 (DUT), Router + * - Pass Criteria: + * - N/A + */ + Log("Step 0: Topology formation – BR_1 (DUT), Router"); + + 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); + + VerifyOrQuit(br1.Get().IsPrimary()); + + hostUla = &host.mInfraIf.FindMatchingAddress("fd00::/8"); + + // Add multicast variables to test info manually to ensure verify script sees them. + for (uint8_t i = 0; i < kTotalNumAddresses; i++) + { + String<16> key; + key.Append("MAS%u", i); + nexus.AddTestVar(key.AsCString(), mas[i].ToString().AsCString()); + } + nexus.AddTestVar("MA2", kMA2); + nexus.AddTestVar("HOST_BACKBONE_ULA", hostUla->ToString().AsCString()); + + for (uint8_t i = 1; i <= kNumBatches; i++) + { + /** + * Step 1 + * - Device: Router + * - Description: Harness instructs the device to register for 15 multicast addresses, MASi. The device + * automatically unicasts an MLR.req CoAP request to the DUT (BR_1) as follows: + * coap://[]:MM/n/mr Where the payload contains: IPv6 Addresses TLV: MASi (15 addresses) + * - Pass Criteria: + * - N/A + */ + Log("Step 1: Router registers 15 multicast addresses, MASi."); + + for (uint8_t j = 0; j < kNumAddressesPerBatch; j++) + { + uint8_t index = (i - 1) * kNumAddressesPerBatch + j; + SuccessOrQuit(router.Get().SubscribeExternalMulticast(mas[index])); + } + + /** + * Step 2 + * - Device: BR_1 (DUT) + * - Description: Automatically responds to the multicast registration. + * - Pass Criteria: + * - The DUT MUST unicast an MLR.rsp CoAP response to Router_1 as follows: 2.04 changed + * - Where the payload contains: Status TLV: 0 [ST_MLR_SUCCESS] + */ + Log("Step 2: BR_1 (DUT) automatically responds to the multicast registration."); + + /** + * Step 3 + * - Device: BR_1 (DUT) + * - Description: Auotmatically informs any other BBRs on the network of the multicast registrations. + * - Pass Criteria: + * - The DUT MUST multicast a BMLR.ntf CoAP request to the Backbone Link, as follows: + * coap://[]:BB/b/bmr + * - Where the payload contains: IPv6 Addresses TLV: MASi (15 addresses) + * - Timeout TLV: default MLR timeout of BR_1 + */ + Log("Step 3: BR_1 (DUT) automatically informs any other BBRs on the network of the multicast registrations."); + // Checks for BLMR.ntf (BMLR.ntf) are intentionally skipped. + + /** + * Step 4 + * - Device: BR_1 (DUT) + * - Description: Automatically multicasts an MLDv2 message. The MLDv2 message may also be sent as multiple + * MLDv2 messages with content distributed across these multiple messages. + * - Pass Criteria: + * - The DUT MUST multicast an MLDv2 message of type “Version 2 Multicast Listener Report” (see [RFC 3810] + * Section 5.2). + * - Where: Nr of Mcast Address Records (M): >= 15 + * - Multicast Address Record [j]: See below + * - Each of the j := 0 … 14 Multicast Address Record containing an address of the set MASi contains the + * following: Record Type: 4 (CHANGE_TO_EXCLUDE_MODE), Number of Sources (N): 0, Multicast Address: MASi[j] + * - Alternatively, the DUT MAY also send multiple of above messages each with a portion of the 15 + * addresses MASi. In this case the Nr of Mcast Address Records can be < 15 but the sum over all messages + * MUST be >= 15. + */ + Log("Step 4: BR_1 (DUT) automatically multicasts an MLDv2 message."); + // Checks for MLDv2 are intentionally skipped. + + nexus.AdvanceTime(kMlrRegistrationTime); + } + + for (uint8_t i = 1; i <= kNumBatches; i++) + { + /** + * Step 5 + * - Device: Host + * - Description: Harness instructs the device to send an ICMPv6 Echo (ping) Request packet to the multicast + * address, MASi[ 3 * i - 1], on the backbone link. + * - Pass Criteria: + * - N/A + */ + Log("Step 5: Host sends an ICMPv6 Echo (ping) Request packet to the multicast address, MASi[ 3 * i - 1]."); + + uint8_t index = (i - 1) * kNumAddressesPerBatch + (3 * i - 1); + host.mInfraIf.SendEchoRequest(*hostUla, mas[index], kEchoIdentifier, kEchoPayloadSize); + nexus.AdvanceTime(100); + + /** + * Step 6 + * - Device: BR_1 (DUT) + * - Description: Automatically forwards the ping request packet to its Thread Network. + * - Pass Criteria: + * - The DUT MUST forward the ICMPv6 Echo (ping) Rquest packet of the previous step to its Thread Network + * encapsulated in an MPL packet, where: + * - MPL Option: If Source outer IP header == BR_1 RLOC Then S == 0 Else S == 1 and seed-id == BR_1 RLOC16 + */ + Log("Step 6: BR_1 (DUT) automatically forwards the ping request packet to its Thread Network."); + + nexus.AdvanceTime(kEchoReplyWaitTime); + } + + /** + * Step 7 + * - Device: Host + * - Description: Harness instructs the device to multicast a packet to the multicast addresses, MA2. + * - Pass Criteria: + * - N/A + */ + Log("Step 7: Host multicasts a packet to the multicast addresses, MA2."); + host.mInfraIf.SendEchoRequest(*hostUla, ma2, kEchoIdentifier, kEchoPayloadSize); + nexus.AdvanceTime(100); + + /** + * Step 8 + * - Device: BR_1 (DUT) + * - Description: Does not forward the packet to its Thread Network. + * - Pass Criteria: + * - The DUT MUST NOT forward the packet with multicast address, MA2, to the Thread Network. + */ + Log("Step 8: BR_1 (DUT) does not forward the packet to its Thread Network."); + + nexus.AdvanceTime(kEchoReplyWaitTime); + + nexus.SaveTestInfo("test_1_2_MATN_TC_16.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestMatnTc16(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_1_2_MATN_TC_16.py b/tests/nexus/verify_1_2_MATN_TC_16.py new file mode 100644 index 000000000..f8713602b --- /dev/null +++ b/tests/nexus/verify_1_2_MATN_TC_16.py @@ -0,0 +1,200 @@ +#!/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 +import struct + +# 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 +from pktverify.bytes import Bytes + + +def verify(pv): + # 5.10.12 MATN-TC-16: Large number of multicast group subscriptions to BBR + # + # 5.10.12.1 Topology + # - BR_1 (DUT) + # - Router + # - Host + # + # 5.10.12.2 Purpose & Description + # The purpose of this test case is to verify that the Primary BBR can handle a large number (75) of subscriptions + # to different multicast groups. Multicast registrations are performed each time with 15 multicast addresses + # per registration request. + # + # Spec Reference | V1.3.0 Section + # ---------------|--------------- + # Multicast | 5.10.12 + + pkts = pv.pkts + pv.summary.show() + + BR_1 = pv.vars['BR_1'] + BR_1_RLOC = pv.vars['BR_1_RLOC'] + BR_1_RLOC16 = pv.vars['BR_1_RLOC16'] + ROUTER = pv.vars['ROUTER'] + MA2 = Ipv6Addr(pv.vars['MA2']) + + NUM_BATCHES = 5 + NUM_ADDRESSES_PER_BATCH = 15 + MLR_URI = '/n/mr' + ST_MLR_SUCCESS = 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") + + for i in range(1, NUM_BATCHES + 1): + mas_i = [ + Ipv6Addr(pv.vars['MAS%d' % ((i - 1) * NUM_ADDRESSES_PER_BATCH + j)]) + for j in range(NUM_ADDRESSES_PER_BATCH) + ] + + # Step 1 + # - Device: Router + # - Description: Harness instructs the device to register for 15 multicast addresses, MASi. The device + # automatically unicasts an MLR.req CoAP request to the DUT (BR_1) as follows: + # coap://[]:MM/n/mr Where the payload contains: IPv6 Addresses TLV: MASi (15 addresses) + # - Pass Criteria: + # - N/A + print("Step 1: Router registers 15 multicast addresses, MASi.") + pkts.filter_wpan_src64(ROUTER).\ + filter_coap_request(MLR_URI).\ + filter(lambda p: all(addr in p.coap.tlv.ipv6_address for addr in mas_i)).\ + must_next() + + # Step 2 + # - Device: BR_1 (DUT) + # - Description: Automatically responds to the multicast registration. + # - Pass Criteria: + # - The DUT MUST unicast an MLR.rsp CoAP response to Router_1 as follows: 2.04 changed + # - Where the payload contains: Status TLV: 0 [ST_MLR_SUCCESS] + print("Step 2: BR_1 (DUT) automatically responds to the multicast registration.") + pkts.filter_wpan_src64(BR_1).\ + filter_coap_ack(MLR_URI).\ + filter(lambda p: p.coap.tlv.status == ST_MLR_SUCCESS).\ + must_next() + + # Step 3 + # - Device: BR_1 (DUT) + # - Description: Auotmatically informs any other BBRs on the network of the multicast registrations. + # - Pass Criteria: + # - The DUT MUST multicast a BMLR.ntf CoAP request to the Backbone Link, as follows: + # coap://[]:BB/b/bmr + # - Where the payload contains: IPv6 Addresses TLV: MASi (15 addresses) + # - Timeout TLV: default MLR timeout of BR_1 + print("Step 3: BR_1 (DUT) automatically informs any other BBRs on the network of the multicast registrations.") + # Checks for BLMR.ntf (BMLR.ntf) are intentionally skipped. + + # Step 4 + # - Device: BR_1 (DUT) + # - Description: Automatically multicasts an MLDv2 message. The MLDv2 message may also be sent as multiple + # MLDv2 messages with content distributed across these multiple messages. + # - Pass Criteria: + # - The DUT MUST multicast an MLDv2 message of type “Version 2 Multicast Listener Report” (see [RFC 3810] + # Section 5.2). + # - Where: Nr of Mcast Address Records (M): >= 15 + # - Multicast Address Record [j]: See below + # - Each of the j := 0 … 14 Multicast Address Record containing an address of the set MASi contains the + # following: Record Type: 4 (CHANGE_TO_EXCLUDE_MODE), Number of Sources (N): 0, Multicast Address: MASi[j] + # - Alternatively, the DUT MAY also send multiple of above messages each with a portion of the 15 + # addresses MASi. In this case the Nr of Mcast Address Records can be < 15 but the sum over all messages + # MUST be >= 15. + print("Step 4: BR_1 (DUT) automatically multicasts an MLDv2 message.") + # Checks for MLDv2 are intentionally skipped. + + for i in range(1, NUM_BATCHES + 1): + mas_i_j = Ipv6Addr(pv.vars['MAS%d' % ((i - 1) * NUM_ADDRESSES_PER_BATCH + (3 * i - 1))]) + + # Step 5 + # - Device: Host + # - Description: Harness instructs the device to send an ICMPv6 Echo (ping) Request packet to the multicast + # address, MASi[ 3 * i - 1], on the backbone link. + # - Pass Criteria: + # - N/A + print("Step 5: Host sends an ICMPv6 Echo (ping) Request packet to the multicast address, MASi[ 3 * i - 1].") + pkts.filter_ping_request().\ + filter_ipv6_dst(mas_i_j).\ + must_next() + + # Step 6 + # - Device: BR_1 (DUT) + # - Description: Automatically forwards the ping request packet to its Thread Network. + # - Pass Criteria: + # - The DUT MUST forward the ICMPv6 Echo (ping) Rquest packet of the previous step to its Thread Network + # encapsulated in an MPL packet, where: + # - MPL Option: If Source outer IP header == BR_1 RLOC Then S == 0 Else S == 1 and seed-id == BR_1 RLOC16 + print("Step 6: BR_1 (DUT) automatically forwards the ping request packet to its Thread Network.") + + def check_step6(p): + if p.ipv6.src == BR_1_RLOC: + return p.ipv6.opt.mpl.flag.s == 0 + else: + return p.ipv6.opt.mpl.flag.s == 1 and \ + p.ipv6.opt.mpl.seed_id == Bytes(struct.pack('>H', BR_1_RLOC16)) + + pkts.filter_wpan_src64(BR_1).\ + filter_ping_request().\ + filter(lambda p: p.ipv6.dst == 'ff03::fc' or p.ipv6.dst == mas_i_j).\ + filter(check_step6).\ + must_next() + + # Step 7 + # - Device: Host + # - Description: Harness instructs the device to multicast a packet to the multicast addresses, MA2. + # - Pass Criteria: + # - N/A + print("Step 7: Host multicasts a packet to the multicast addresses, MA2.") + pkts.filter_ping_request().\ + filter_ipv6_dst(MA2).\ + must_next() + + # Step 8 + # - Device: BR_1 (DUT) + # - Description: Does not forward the packet to its Thread Network. + # - Pass Criteria: + # - The DUT MUST NOT forward the packet with multicast address, MA2, to the Thread Network. + print("Step 8: BR_1 (DUT) does not forward the packet to its Thread Network.") + pkts.copy().\ + filter_wpan_src64(BR_1).\ + filter_ping_request().\ + filter(lambda p: p.ipv6.dst == 'ff03::fc' or p.ipv6.dst == MA2).\ + must_not_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify)