From 37c6808380d1c26f87caec97f295774a872e4254 Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Mon, 23 Mar 2026 17:21:52 -0500 Subject: [PATCH] [nexus] add DBR-TC-06 test and support for Router Advertisements (#12742) This commit introduces the 1_3_DBR_TC_6 Nexus test case to verify bi-directional reachability in a multi-BR topology with existing IPv6 infrastructure. Key changes: - Implement tests/nexus/test_1_3_DBR_TC_6.cpp and its corresponding pcap-based verification script tests/nexus/verify_1_3_DBR_TC_6.py. - Enhance the Nexus platform InfraIf class to support constructing and sending ICMPv6 Router Advertisements (RA) with PIO and RIO. - Add RouterAdvertisementStart() and RouterAdvertisementStop() to InfraIf for managed periodic unsolicited RA transmissions. - Update Core::Process() to drive the periodic RA logic in InfraIf. - Implement response logic for ICMPv6 Router Solicitations in InfraIf when RA advertising is enabled. - The 1_3_DBR_TC_6 test validates that the DUT BR correctly adopts existing OMR prefixes, advertises an external default route (::/0), and sends appropriate RAs on the infrastructure link. - Register the new test case in tests/nexus/CMakeLists.txt and tests/nexus/run_nexus_tests.sh. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/platform/nexus_infra_if.cpp | 78 ++++++- tests/nexus/platform/nexus_infra_if.hpp | 20 +- tests/nexus/platform/nexus_node.hpp | 13 +- tests/nexus/run_nexus_tests.sh | 1 + tests/nexus/test_1_3_DBR_TC_6.cpp | 287 ++++++++++++++++++++++++ tests/nexus/verify_1_3_DBR_TC_6.py | 264 ++++++++++++++++++++++ 7 files changed, 656 insertions(+), 8 deletions(-) create mode 100644 tests/nexus/test_1_3_DBR_TC_6.cpp create mode 100644 tests/nexus/verify_1_3_DBR_TC_6.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 3815ab38c..3fe08c390 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -257,6 +257,7 @@ ot_nexus_test(1_2_BBR_TC_3 "cert;nexus") ot_nexus_test(1_3_DBR_TC_1 "cert;nexus") ot_nexus_test(1_3_DBR_TC_2 "cert;nexus") ot_nexus_test(1_3_DBR_TC_3 "cert;nexus") +ot_nexus_test(1_3_DBR_TC_6 "cert;nexus") # Misc tests ot_nexus_test(border_admitter "core;nexus") diff --git a/tests/nexus/platform/nexus_infra_if.cpp b/tests/nexus/platform/nexus_infra_if.cpp index cabd8a2aa..518812d53 100644 --- a/tests/nexus/platform/nexus_infra_if.cpp +++ b/tests/nexus/platform/nexus_infra_if.cpp @@ -34,10 +34,12 @@ namespace ot { namespace Nexus { -InfraIf::InfraIf(void) +InfraIf::InfraIf(Instance &aInstance) : mNode(nullptr) , mNodeId(0) , mIfIndex(0) + , mHasRioPrefix(false) + , mRaTimer(aInstance) { } @@ -152,12 +154,69 @@ void InfraIf::SendIcmp6Nd(const Ip6::Address &aDestAddress, const uint8_t *aBuff mPendingTxQueue.Enqueue(*message); } +void InfraIf::SendRouterAdvertisement(const Ip6::Address &aDestination, + const Ip6::Prefix *aPioPrefix, + const Ip6::Prefix *aRioPrefix) +{ + Ip6::Nd::RouterAdvert::TxMessage ra; + Ip6::Nd::RouterAdvert::Header header; + Ip6::Nd::Icmp6Packet packet; + + header.SetToDefault(); + header.SetRouterLifetime(1800); + SuccessOrQuit(ra.Append(header)); + + if (aPioPrefix != nullptr) + { + SuccessOrQuit(ra.AppendPrefixInfoOption(*aPioPrefix, 1800, 1800, + Ip6::Nd::PrefixInfoOption::kOnLinkFlag | + Ip6::Nd::PrefixInfoOption::kAutoConfigFlag)); + } + + if (aRioPrefix != nullptr) + { + SuccessOrQuit(ra.AppendRouteInfoOption(*aRioPrefix, 1800, NetworkData::kRoutePreferenceMedium)); + } + + ra.GetAsPacket(packet); + + SendIcmp6Nd(aDestination, packet.GetBytes(), packet.GetLength()); +} + +void InfraIf::StartRouterAdvertisement(const Ip6::Prefix &aPioPrefix, const Ip6::Prefix *aRioPrefix) +{ + mPioPrefix = aPioPrefix; + if (aRioPrefix != nullptr) + { + mRioPrefix = *aRioPrefix; + mHasRioPrefix = true; + } + else + { + mHasRioPrefix = false; + } + + // Trigger initial RA immediately + SendPeriodicRouterAdvertisement(); +} + +void InfraIf::StopRouterAdvertisement(void) { mRaTimer.Stop(); } + +void InfraIf::HandleRaTimer(void) { SendPeriodicRouterAdvertisement(); } + +void InfraIf::SendPeriodicRouterAdvertisement(void) +{ + const uint32_t kRaInterval = 4000; // 4 seconds in milliseconds + + SendRouterAdvertisement(Ip6::Address::GetLinkLocalAllNodesMulticast(), &mPioPrefix, + mHasRioPrefix ? &mRioPrefix : nullptr); + mRaTimer.Start(kRaInterval); +} + void InfraIf::ProcessIcmp6Nd(const Ip6::Address &aSrcAddress, const uint8_t *aBuffer, uint16_t aBufferLength) { Ip6::Nd::Icmp6Packet packet; - OT_UNUSED_VARIABLE(aSrcAddress); - packet.Init(aBuffer, aBufferLength); VerifyOrExit(packet.GetLength() >= sizeof(Ip6::Icmp::Header)); @@ -181,6 +240,9 @@ void InfraIf::ProcessIcmp6Nd(const Ip6::Address &aSrcAddress, const uint8_t *aBu } case Ip6::Icmp::Header::kTypeRouterSolicit: + HandleRouterSolicitation(aSrcAddress); + break; + case Ip6::Icmp::Header::kTypeNeighborAdvert: case Ip6::Icmp::Header::kTypeNeighborSolicit: // TODO: Handle other ND messages as needed for the simulation. @@ -237,6 +299,16 @@ exit: return; } +void InfraIf::HandleRouterSolicitation(const Ip6::Address &aSrcAddress) +{ + if (mRaTimer.IsRunning()) + { + const Ip6::Address &dest = + aSrcAddress.IsUnspecified() ? Ip6::Address::GetLinkLocalAllNodesMulticast() : aSrcAddress; + SendRouterAdvertisement(dest, &mPioPrefix, mHasRioPrefix ? &mRioPrefix : nullptr); + } +} + void InfraIf::SendIp6(const Ip6::Address &aSrcAddress, const Ip6::Address &aDestAddress, const uint8_t *aBuffer, diff --git a/tests/nexus/platform/nexus_infra_if.hpp b/tests/nexus/platform/nexus_infra_if.hpp index b9a2c73f3..6e2074440 100644 --- a/tests/nexus/platform/nexus_infra_if.hpp +++ b/tests/nexus/platform/nexus_infra_if.hpp @@ -29,7 +29,6 @@ #ifndef OT_NEXUS_PLATFORM_NEXUS_INFRA_IF_HPP_ #define OT_NEXUS_PLATFORM_NEXUS_INFRA_IF_HPP_ -#include #include "instance/instance.hpp" namespace ot { @@ -42,7 +41,7 @@ class InfraIf public: typedef otPlatInfraIfLinkLayerAddress LinkLayerAddress; - InfraIf(void); + explicit InfraIf(Instance &aInstance); void Init(Node &aNode); @@ -60,6 +59,11 @@ public: const Heap::Array &GetAddresses(void) const { return mAddresses; } void SendIcmp6Nd(const Ip6::Address &aDestAddress, const uint8_t *aBuffer, uint16_t aBufferLength); + void SendRouterAdvertisement(const Ip6::Address &aDestination, + const Ip6::Prefix *aPioPrefix, + const Ip6::Prefix *aRioPrefix); + void StartRouterAdvertisement(const Ip6::Prefix &aPioPrefix, const Ip6::Prefix *aRioPrefix = nullptr); + void StopRouterAdvertisement(void); void SendIp6(const Ip6::Address &aSrcAddress, const Ip6::Address &aDestAddress, const uint8_t *aBuffer, @@ -94,15 +98,27 @@ public: private: void ProcessIcmp6Nd(const Ip6::Address &aSrcAddress, const uint8_t *aBuffer, uint16_t aBufferLength); + void SendPeriodicRouterAdvertisement(void); void HandlePrefixInfoOption(const Ip6::Nd::PrefixInfoOption &aPio); + void HandleRouterSolicitation(const Ip6::Address &aSrcAddress); void HandleEchoRequest(const Ip6::Header &aHeader, Message &aMessage); void HandleEchoReply(const Ip6::Header &aHeader, Message &aMessage); + void HandleRaTimer(void); + Node *mNode; uint32_t mNodeId; uint32_t mIfIndex; Heap::Array mAddresses; Callback mEchoReplyCallback; + + Ip6::Prefix mPioPrefix; + Ip6::Prefix mRioPrefix; + bool mHasRioPrefix; + + using RaTimer = TimerMilliIn; + + RaTimer mRaTimer; }; } // namespace Nexus diff --git a/tests/nexus/platform/nexus_node.hpp b/tests/nexus/platform/nexus_node.hpp index 5f36e1bbb..4055e46dc 100644 --- a/tests/nexus/platform/nexus_node.hpp +++ b/tests/nexus/platform/nexus_node.hpp @@ -58,8 +58,9 @@ public: bool mPendingTasklet; protected: - Platform(void) - : mPendingTasklet(false) + explicit Platform(Instance &aInstance) + : mInfraIf(aInstance) + , mPendingTasklet(false) { } }; @@ -153,7 +154,10 @@ public: Node *mNext; private: - Node(void) {} + Node(void) + : Platform(static_cast(*this)) + { + } String<32> mName; }; @@ -161,6 +165,9 @@ private: inline Node &AsNode(otInstance *aInstance) { return Node::From(aInstance); } } // namespace Nexus + +template <> inline Nexus::InfraIf &Instance::Get(void) { return static_cast(this)->mInfraIf; } + } // namespace ot #endif // OT_NEXUS_PLATFORM_NEXUS_NODE_HPP_ diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index da3a513b4..1f6126e01 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -193,6 +193,7 @@ DEFAULT_TESTS=( "1_3_DBR_TC_1" "1_3_DBR_TC_2" "1_3_DBR_TC_3" + "1_3_DBR_TC_6" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_1_3_DBR_TC_6.cpp b/tests/nexus/test_1_3_DBR_TC_6.cpp new file mode 100644 index 000000000..023bca07d --- /dev/null +++ b/tests/nexus/test_1_3_DBR_TC_6.cpp @@ -0,0 +1,287 @@ +/* + * 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 = 13 * 1000; + +/** + * Time to advance for a node to join as a child and upgrade to a router, in milliseconds. + */ +static constexpr uint32_t kJoinNetworkTime = 200 * 1000; + +/** + * Time to advance for the BR to perform automatic actions (RA, Network Data), in milliseconds. + */ +static constexpr uint32_t kBrActionTime = 30 * 1000; + +/** + * Time to advance for the network to stabilize, in milliseconds. + */ +static constexpr uint32_t kStabilizationTime = 60 * 1000; + +/** + * Time to advance for the ping response, in milliseconds. + */ +static constexpr uint32_t kPingResponseTime = 5 * 1000; + +/** + * Infrastructure interface index. + */ +static constexpr uint32_t kInfraIfIndex = 1; + +/** + * Echo Request identifier. + */ +static constexpr uint16_t kEchoIdentifier = 0x1234; + +/** + * Echo Request payload size. + */ +static constexpr uint16_t kEchoPayloadSize = 10; + +/** + * IPv6 GUA address for Eth_1. + */ +static const char kEth1Gua[] = "2001:db8:1::1"; + +/** + * IPv6 GUA prefix GUA_1. + */ +static const char kGua1Prefix[] = "2001:db8:1::/64"; + +void Test_1_3_DBR_TC_6(void) +{ + /** + * 1.6. [1.3] [CERT] Reachability - Multiple BRs - Single Thread - Single IPv6 Infrastructure + * + * 1.6.1. Purpose + * To test the following: + * - 1. Bi-directional reachability between single Thread Network and infrastructure devices + * - 2. Multiple BRs + * - 3. IPv6 infrastructure is already existing + * - 4. DUT BR adopts existing OMR prefixes and doesn't advertise PIO + * + * 1.6.2. Topology + * - 1. BR 1 (DUT) - Thread Border Router + * - 2. BR 2-Test Bed border router device operating as a Thread Border Router and the Leader + * - 3. ED 1-Test Bed device operating as a Thread End Device, attached to BR_1 + * - 4. Eth 1-Test Bed border router device on an Adjacent Infrastructure Link + * + * Spec Reference | V1.3.0 Section + * ---------------|--------------- + * Reachability | 1.3 + */ + + Core nexus; + + Node &br1 = nexus.CreateNode(); + Node &br2 = nexus.CreateNode(); + Node &ed1 = nexus.CreateNode(); + Node ð1 = nexus.CreateNode(); + + br1.SetName("BR_1"); + br2.SetName("BR_2"); + ed1.SetName("ED_1"); + eth1.SetName("Eth_1"); + + nexus.AdvanceTime(0); + + Instance::SetLogLevel(kLogLevelNote); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 0: Device: Eth 1 Description (DBR-1.6): Harness configures Ethernet link with an on-link IPv6 GUA prefix " + "GUA 1. Eth 1 is configured to multicast ND RAS."); + + eth1.mInfraIf.Init(eth1); + + { + Ip6::Address eth1Gua; + SuccessOrQuit(eth1Gua.FromString(kEth1Gua)); + eth1.mInfraIf.AddAddress(eth1Gua); + } + + { + Ip6::Prefix gua1; + SuccessOrQuit(gua1.FromString(kGua1Prefix)); + eth1.mInfraIf.StartRouterAdvertisement(gua1); + } + + nexus.AdvanceTime(10 * 1000); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: Device: Eth 1, BR 2 Description (DBR-1.6): Form topology. Wait for BR_2 to: 1. Register as border " + "router in Thread Network Data with an OMR prefix OMR_1 2. Send multicast ND RAS"); + + br2.mInfraIf.Init(br2); + br2.Get().Init(kInfraIfIndex, true); + + br2.Get().Init(); + SuccessOrQuit(br2.Get().SetEnabled(true)); + + br2.Form(); + nexus.AdvanceTime(kFormNetworkTime); + + nexus.AdvanceTime(kBrActionTime); + + Ip6::Prefix omr1; + SuccessOrQuit(br2.Get().GetOmrPrefix(omr1)); + + br2.mInfraIf.SendRouterAdvertisement(Ip6::Address::GetLinkLocalAllNodesMulticast(), nullptr, &omr1); + nexus.AdvanceTime(kBrActionTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: Device: BR 1 (DUT) Description (DBR-1.6): Enable: switch on."); + + br1.mInfraIf.Init(br1); + br1.Get().Init(kInfraIfIndex, true); + + br1.AllowList(br2); + br2.AllowList(br1); + + br1.Join(br2); + nexus.AdvanceTime(kJoinNetworkTime); + + br1.Get().Init(); + SuccessOrQuit(br1.Get().SetEnabled(true)); + + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2b: Device: ED 1 Description (DBR-1.6): Harness enables device."); + + ed1.AllowList(br1); + br1.AllowList(ed1); + + ed1.Join(br1, Node::kAsFed); + nexus.AdvanceTime(kJoinNetworkTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: Device: BR 1 (DUT) Description (DBR-1.6): Automatically registers itself as a border router in the " + "Thread Network Data."); + + nexus.AdvanceTime(kBrActionTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Device: BR 1 (DUT) Description (DBR-1.6): Automatically multicasts ND RAs on Adjacent Infrastructure " + "Link."); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: Device: Eth_1 Description (DBR-1.6): Harness instructs the device to send an ICMPv6 Echo Request to " + "ED 1 via BR 1 or BR 2. 1. IPv6 Source: Eth 1 GUA 2. IPv6 Destination: ED_1 OMR"); + + const Ip6::Address &ed1Omr = ed1.FindMatchingAddress(omr1.ToString().AsCString()); + Ip6::Address eth1Gua; + + SuccessOrQuit(eth1Gua.FromString(kEth1Gua)); + + eth1.mInfraIf.SendEchoRequest(eth1Gua, ed1Omr, kEchoIdentifier, kEchoPayloadSize); + nexus.AdvanceTime(kPingResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: Device: ED_1 Description (DBR-1.6): Harness instructs the device to send an ICMPv6 Echo Request to " + "Eth_1. 1. IPv6 Source: ED 1 OMR 2. IPv6 Destination: Eth_1 GUA"); + + ed1.SendEchoRequest(eth1Gua, kEchoIdentifier, kEchoPayloadSize, 64, &ed1Omr); + nexus.AdvanceTime(kPingResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 7: Device: BR 2 Description (DBR-1.6): Harness disables the device."); + + br2.Get().Stop(); + nexus.AdvanceTime(10 * 1000); + nexus.AdvanceTime(kBrActionTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 8: Device: BR 1 (DUT) Description (DBR-1.6): Repeat Step 4"); + + { + Ip6::Nd::RouterSolicitHeader rs; + Ip6::Nd::Icmp6Packet packet; + + packet.Init(reinterpret_cast(&rs), sizeof(rs)); + eth1.mInfraIf.SendIcmp6Nd(Ip6::Address::GetLinkLocalAllRoutersMulticast(), packet.GetBytes(), + packet.GetLength()); + } + nexus.AdvanceTime(kBrActionTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 9: Device: Eth 1 Description (DBR-1.6): Repeat Step 5"); + + eth1.mInfraIf.SendEchoRequest(eth1Gua, ed1Omr, kEchoIdentifier, kEchoPayloadSize); + nexus.AdvanceTime(kPingResponseTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 10: Device: ED 1 Description (DBR-1.6): Repeat Step 6"); + + ed1.SendEchoRequest(eth1Gua, kEchoIdentifier, kEchoPayloadSize, 64, &ed1Omr); + nexus.AdvanceTime(kPingResponseTime); + + { + char macStr[18]; + InfraIf::LinkLayerAddress addr; + + br1.mInfraIf.GetLinkLayerAddress(addr); + snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", addr.mAddress[0], addr.mAddress[1], + addr.mAddress[2], addr.mAddress[3], addr.mAddress[4], addr.mAddress[5]); + nexus.AddTestVar("BR1_ETH", macStr); + + eth1.mInfraIf.GetLinkLayerAddress(addr); + snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", addr.mAddress[0], addr.mAddress[1], + addr.mAddress[2], addr.mAddress[3], addr.mAddress[4], addr.mAddress[5]); + nexus.AddTestVar("ETH1_ETH", macStr); + } + + nexus.AddTestVar("BR1", br1.Get().GetExtAddress().ToString().AsCString()); + nexus.AddTestVar("BR2", br2.Get().GetExtAddress().ToString().AsCString()); + nexus.AddTestVar("ED1", ed1.Get().GetExtAddress().ToString().AsCString()); + nexus.AddTestVar("ETH1_GUA", kEth1Gua); + nexus.AddTestVar("ED1_OMR", ed1Omr.ToString().AsCString()); + nexus.AddTestVar("OMR_PREFIX", omr1.ToString().AsCString()); + + nexus.SaveTestInfo("test_1_3_DBR_TC_6.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::Test_1_3_DBR_TC_6(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_1_3_DBR_TC_6.py b/tests/nexus/verify_1_3_DBR_TC_6.py new file mode 100644 index 000000000..bf5c0cf56 --- /dev/null +++ b/tests/nexus/verify_1_3_DBR_TC_6.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2026, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +import sys +import os + +# Add the current directory to sys.path to find verify_utils +CUR_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(CUR_DIR) + +import verify_utils +from pktverify import consts +from pktverify.addrs import Ipv6Addr + + +def verify(pv): + # 1.6. [1.3] [CERT] Reachability - Multiple BRs - Single Thread - Single IPv6 Infrastructure + # + # 1.6.1. Purpose + # To test the following: + # - 1. Bi-directional reachability between single Thread Network and infrastructure devices + # - 2. Multiple BRs + # - 3. IPv6 infrastructure is already existing + # - 4. DUT BR adopts existing OMR prefixes and doesn't advertise PIO + # + # 1.6.2. Topology + # - 1. BR 1 (DUT) - Thread Border Router + # - 2. BR 2-Test Bed border router device operating as a Thread Border Router and the Leader + # - 3. ED 1-Test Bed device operating as a Thread End Device, attached to BR_1 + # - 4. Eth 1-Test Bed border router device on an Adjacent Infrastructure Link + # + # Spec Reference | V1.3.0 Section + # ---------------|--------------- + # Reachability | 1.3 + + pkts = pv.pkts + pv.summary.show() + + BR1 = pv.vars['BR1'] + ED1 = pv.vars['ED1'] + OMR_PREFIX = Ipv6Addr(pv.vars['OMR_PREFIX'].split('/')[0]) + ETH1_GUA = Ipv6Addr(pv.vars['ETH1_GUA']) + ED1_OMR = Ipv6Addr(pv.vars['ED1_OMR']) + + # Step 0 + # Device: Eth 1 + # Description (DBR-1.6): Harness configures Ethernet link with an on-link IPv6 GUA prefix GUA 1. + # Eth 1 is configured to multicast ND RAS. + # Pass Criteria: + # N/A + print("Step 0: Harness configures Ethernet link with an on-link IPv6 GUA prefix GUA 1.") + + # Step 1 + # Device: Eth 1, BR 2 + # Description (DBR-1.6): Form topology. Wait for BR_2 to: 1. Register as border router in Thread + # Network Data with an OMR prefix OMR_1 2. Send multicast ND RAS + # Pass Criteria: + # N/A + print("Step 1: Form topology. Wait for BR_2 to register as border router and send ND RAs.") + + # Step 2 + # Device: BR 1 (DUT) + # Description (DBR-1.6): Enable: switch on. + # Pass Criteria: + # N/A + print("Step 2: Enable: switch on.") + + # Step 3 + # Device: BR 1 (DUT) + # Description (DBR-1.6): Automatically registers itself as a border router in the Thread Network Data. + # Pass Criteria: + # - The DUT MUST NOT register a new OMR Prefix in the Thread Network Data. + # - The DUT MUST advertise an external route in the Thread Network Data as follows: + # - Prefix: ::/0 (zero-length prefix) + # - Has Route TLV + # - Prf 'Medium' (00) or 'Low' (11) + print("Step 3: BR 1 (DUT) MUST NOT register a new OMR Prefix. MUST advertise an external route ::/0.") + + def check_step3_nwd(p): + if not (hasattr(p, 'mle') and p.mle.cmd == consts.MLE_DATA_RESPONSE): + return False + + prefixes = verify_utils.as_list(p.thread_nwd.tlv.prefix) + # Check for ::/0 + if Ipv6Addr('::') not in prefixes: + return False + + # Check that ONLY ONE OMR prefix exists (the one from BR2) + omr_prefixes = [pref for pref in prefixes if pref != Ipv6Addr('::')] + if len(omr_prefixes) != 1: + return False + + # Check Preference of ::/0 (Has Route TLV) + try: + # Find the index of :: prefix + idx = prefixes.index(Ipv6Addr('::')) + # Preference should be Medium (0) or Low (3 in some dissectors, or 11 binary) + pref = verify_utils.as_list(p.thread_nwd.tlv.has_route.pref)[idx] + if pref not in (0, 3): + return False + except (AttributeError, IndexError): + pass + + return True + + pkts.filter_wpan_src64(BR1).\ + filter(check_step3_nwd).\ + must_next() + + # Step 2b + # Device: ED 1 + # Description (DBR-1.6): Harness enables device. + # Pass Criteria: + # - ED_1 successfully attaches to the DUT as its Parent. + print("Step 2b: ED_1 successfully attaches to the DUT as its Parent.") + pkts.filter_wpan_src64(ED1).\ + filter(lambda p: hasattr(p, 'mle') and p.mle.cmd == consts.MLE_PARENT_REQUEST).\ + must_next() + pkts.filter_wpan_src64(BR1).\ + filter(lambda p: hasattr(p, 'mle') and p.mle.cmd == consts.MLE_PARENT_RESPONSE).\ + must_next() + pkts.filter_wpan_src64(ED1).\ + filter(lambda p: hasattr(p, 'mle') and p.mle.cmd == consts.MLE_CHILD_ID_REQUEST).\ + must_next() + pkts.filter_wpan_src64(BR1).\ + filter(lambda p: hasattr(p, 'mle') and p.mle.cmd == consts.MLE_CHILD_ID_RESPONSE).\ + must_next() + + # Step 4 + # Device: BR 1 (DUT) + # Description (DBR-1.6): Automatically multicasts ND RAs on Adjacent Infrastructure Link. + # Pass Criteria: + # - The DUT MUST multicast ND RAS: + # - IPv6 destination MUST be ff02::1 + # - MUST NOT contain a Prefix Information Option (PIO). + # - MUST contain a Route Information Option (RIO) with the OMR prefix OMR 1. + print("Step 4: BR 1 (DUT) MUST multicast ND RAs: ff02::1, NO PIO, RIO with OMR_1.") + pkts.filter_eth_src(pv.vars['BR1_ETH']).\ + filter_ipv6_dst("ff02::1").\ + filter(lambda p: p.icmpv6.type == verify_utils.ICMPV6_TYPE_ROUTER_ADVERTISEMENT).\ + filter(lambda p: OMR_PREFIX in verify_utils.get_ra_prefixes(p)[0]).\ + filter(lambda p: len(verify_utils.get_ra_prefixes(p)[1]) == 0).\ + must_next() + + # Step 5 + # Device: Eth_1 + # Description (DBR-1.6): Harness instructs the device to send an ICMPv6 Echo Request to ED 1 via BR 1 or + # BR 2. 1. IPv6 Source: Eth 1 GUA 2. IPv6 Destination: ED_1 OMR + # Pass Criteria: + # - Eth_1 receives an ICMPv6 Echo Reply from ED_1. + # - IPv6 Source: ED_1 OMR + # - IPv6 Destination: Eth 1 GUA + print("Step 5: Eth_1 pings ED_1 OMR.") + _pkt = pkts.filter_ipv6_src(ETH1_GUA).\ + filter_ipv6_dst(ED1_OMR).\ + filter_ping_request().\ + must_next() + + pkts.filter_ipv6_src(ED1_OMR).\ + filter_ipv6_dst(ETH1_GUA).\ + filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).\ + must_next() + + # Step 6 + # Device: ED_1 + # Description (DBR-1.6): Harness instructs the device to send an ICMPv6 Echo Request to Eth_1. + # 1. IPv6 Source: ED 1 OMR 2. IPv6 Destination: Eth_1 GUA + # Pass Criteria: + # - ED_1 receives an ICMPv6 Echo Reply from Eth_1. + # - IPv6 Source: Eth_1 GUA + # - IPv6 Destination: ED 1 OMR + print("Step 6: ED_1 pings Eth_1 GUA.") + _pkt = pkts.filter_ipv6_src(ED1_OMR).\ + filter_ipv6_dst(ETH1_GUA).\ + filter_ping_request().\ + must_next() + + pkts.filter_ipv6_src(ETH1_GUA).\ + filter_ipv6_dst(ED1_OMR).\ + filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).\ + must_next() + + # Step 7 + # Device: BR 2 + # Description (DBR-1.6): Harness disables the device. Note: automatically, the network data of BR_2 + # including the prefix OMR 1 data will remain active for the remainder of this test procedure. + # Pass Criteria: + # N/A + print("Step 7: Harness disables BR 2.") + + # Step 8 + # Device: BR 1 (DUT) + # Description (DBR-1.6): Repeat Step 4 + # Pass Criteria: + # - Repeat Step 4 + print("Step 8: Repeat Step 4") + pkts.filter_eth_src(pv.vars['BR1_ETH']).\ + filter_ipv6_dst("ff02::1").\ + filter(lambda p: p.icmpv6.type == verify_utils.ICMPV6_TYPE_ROUTER_ADVERTISEMENT).\ + filter(lambda p: OMR_PREFIX in verify_utils.get_ra_prefixes(p)[0]).\ + filter(lambda p: len(verify_utils.get_ra_prefixes(p)[1]) == 0).\ + must_next() + + # Step 9 + # Device: Eth 1 + # Description (DBR-1.6): Repeat Step 5 + # Pass Criteria: + # - Repeat Step 5 + print("Step 9: Repeat Step 5") + _pkt = pkts.filter_ipv6_src(ETH1_GUA).\ + filter_ipv6_dst(ED1_OMR).\ + filter_ping_request().\ + must_next() + + pkts.filter_ipv6_src(ED1_OMR).\ + filter_ipv6_dst(ETH1_GUA).\ + filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).\ + must_next() + + # Step 10 + # Device: ED 1 + # Description (DBR-1.6): Repeat Step 6 + # Pass Criteria: + # - Repeat Step 6 + print("Step 10: Repeat Step 6") + _pkt = pkts.filter_ipv6_src(ED1_OMR).\ + filter_ipv6_dst(ETH1_GUA).\ + filter_ping_request().\ + must_next() + + pkts.filter_ipv6_src(ETH1_GUA).\ + filter_ipv6_dst(ED1_OMR).\ + filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).\ + must_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify)