diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 52b6ac18f..2322f0cd9 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -397,6 +397,7 @@ ot_nexus_test(mle_blocking_downgrade "core;nexus") ot_nexus_test(nat64_translator "core;nexus") ot_nexus_test(router_downgrade_on_sec_policy_change "core;nexus") ot_nexus_test(srp_lease "core;nexus") +ot_nexus_test(zero_len_external_route "core;nexus") # Trel ot_nexus_test(trel "trel;nexus") diff --git a/tests/nexus/platform/nexus_core.cpp b/tests/nexus/platform/nexus_core.cpp index 6e09158e1..f8b1a7e04 100644 --- a/tests/nexus/platform/nexus_core.cpp +++ b/tests/nexus/platform/nexus_core.cpp @@ -1061,9 +1061,10 @@ void Core::SendAndVerifyEchoRequest(Node &aSender, aSender.SendEchoRequest(aDestination, kIdentifier, aPayloadSize, aHopLimit); AdvanceTime(aResponseTimeout); - VerifyOrQuit(icmpContext.mResponseReceived); SuccessOrQuit(aSender.Get().UnregisterHandler(icmpHandler)); + + VerifyOrQuit(icmpContext.mResponseReceived); } void Core::SendAndVerifyEchoRequest(Node &aSender, diff --git a/tests/nexus/test_zero_len_external_route.cpp b/tests/nexus/test_zero_len_external_route.cpp new file mode 100644 index 000000000..940aa9c28 --- /dev/null +++ b/tests/nexus/test_zero_len_external_route.cpp @@ -0,0 +1,299 @@ +/* + * 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" +#include "thread/network_data_local.hpp" +#include "thread/network_data_notifier.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 kAttachToRouterTime = 200 * 1000; + +/** + * Time to advance for the network to stabilize after nodes have attached. + */ +static constexpr uint32_t kStabilizationTime = 10 * 1000; + +/** + * Time to wait for ICMPv6 Echo response. + */ +static constexpr uint32_t kEchoResponseWaitTime = 5 * 1000; + +/** + * ICMPv6 Echo Request payload size, in bytes. + */ +static constexpr uint16_t kEchoPayloadSize = 0; + +/** + * Add an on-mesh prefix to a node. + */ +static void AddPrefix(Node &aNode, const char *aPrefixString, bool aDefaultRoute) +{ + NetworkData::OnMeshPrefixConfig config; + + config.Clear(); + SuccessOrQuit(config.GetPrefix().FromString(aPrefixString)); + config.mOnMesh = true; + config.mStable = true; + config.mPreferred = true; + config.mSlaac = true; + config.mDefaultRoute = aDefaultRoute; + + SuccessOrQuit(aNode.Get().AddOnMeshPrefix(config)); + aNode.Get().HandleServerDataUpdated(); +} + +/** + * Remove an on-mesh prefix from a node. + */ +static void RemovePrefix(Node &aNode, const char *aPrefixString) +{ + Ip6::Prefix prefix; + SuccessOrQuit(prefix.FromString(aPrefixString)); + SuccessOrQuit(aNode.Get().RemoveOnMeshPrefix(prefix)); + aNode.Get().HandleServerDataUpdated(); +} + +/** + * Add an external route to a node. + */ +static void AddExternalRoute(Node &aNode, const char *aPrefixString) +{ + NetworkData::ExternalRouteConfig config; + + config.Clear(); + SuccessOrQuit(config.GetPrefix().FromString(aPrefixString)); + config.mStable = true; + config.mPreference = NetworkData::kRoutePreferenceMedium; + + SuccessOrQuit(aNode.Get().AddHasRoutePrefix(config)); + aNode.Get().HandleServerDataUpdated(); +} + +/** + * Remove an external route from a node. + */ +static void RemoveExternalRoute(Node &aNode, const char *aPrefixString) +{ + Ip6::Prefix prefix; + SuccessOrQuit(prefix.FromString(aPrefixString)); + SuccessOrQuit(aNode.Get().RemoveHasRoutePrefix(prefix)); + aNode.Get().HandleServerDataUpdated(); +} + +/** + * Add a manual IPv6 address to a node. + */ +static void AddIpAddress(Node &aNode, otNetifAddress &aAddress, const char *aAddressString) +{ + ClearAllBytes(aAddress); + SuccessOrQuit(AsCoreType(&aAddress.mAddress).FromString(aAddressString)); + aAddress.mPrefixLength = 64; + aAddress.mAddressOrigin = OT_ADDRESS_ORIGIN_MANUAL; + aAddress.mPreferred = true; + aAddress.mValid = true; + + SuccessOrQuit(otIp6AddUnicastAddress(&aNode.GetInstance(), &aAddress)); +} + +/** + * Remove a manual IPv6 address from a node. + */ +static void RemoveIpAddress(Node &aNode, const char *aAddressString) +{ + Ip6::Address address; + SuccessOrQuit(address.FromString(aAddressString)); + SuccessOrQuit(otIp6RemoveUnicastAddress(&aNode.GetInstance(), &address)); +} + +struct IcmpResponseContext +{ + IcmpResponseContext(uint16_t aIdentifier) + : mIdentifier(aIdentifier) + , mResponseReceived(false) + { + } + + uint16_t mIdentifier; + bool mResponseReceived; +}; + +static void HandleIcmpResponse(void *aContext, otMessage *, const otMessageInfo *, const otIcmp6Header *aIcmpHeader) +{ + IcmpResponseContext *context = static_cast(aContext); + const Ip6::Icmp::Header *header = AsCoreTypePtr(aIcmpHeader); + + if ((header->GetType() == Ip6::Icmp::Header::kTypeEchoReply) && (header->GetId() == context->mIdentifier)) + { + context->mResponseReceived = true; + } +} + +/** + * Send an ICMPv6 Echo Request and verify that NO response is received. + */ +static void SendAndVerifyNoEchoResponse(Core &aNexus, + Node &aSender, + const Ip6::Address &aDestination, + uint32_t aWaitTime) +{ + static constexpr uint16_t kIdentifier = 0xabcd; + + IcmpResponseContext icmpContext(kIdentifier); + Ip6::Icmp::Handler icmpHandler(HandleIcmpResponse, &icmpContext); + + SuccessOrQuit(aSender.Get().RegisterHandler(icmpHandler)); + + aSender.SendEchoRequest(aDestination, kIdentifier); + aNexus.AdvanceTime(aWaitTime); + + SuccessOrQuit(aSender.Get().UnregisterHandler(icmpHandler)); + + VerifyOrQuit(!icmpContext.mResponseReceived, "Received unexpected Echo Reply"); +} + +void TestZeroLenExternalRoute(void) +{ + Core nexus; + otNetifAddress router1Addr; + otNetifAddress router2Addr; + + Node &leader = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + Node &router2 = nexus.CreateNode(); + + leader.SetName("LEADER"); + router1.SetName("ROUTER_1"); + router2.SetName("ROUTER_2"); + + nexus.AdvanceTime(0); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote)); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: Form network and join routers"); + + leader.Form(); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(leader.Get().IsLeader()); + + router1.Join(leader); + router2.Join(leader); + nexus.AdvanceTime(kAttachToRouterTime); + VerifyOrQuit(router1.Get().IsRouter()); + VerifyOrQuit(router2.Get().IsRouter()); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: Add prefix on ROUTER_2 and external route and address on ROUTER_1"); + + // Add an on-mesh prefix with SLAAC on router2 (without default route flag). + AddPrefix(router2, "fd00:1234::/64", false); + + // Add a zero length external route on router1. Also add an IPv6 + // address on router1. + AddExternalRoute(router1, "::/0"); + AddIpAddress(router1, router1Addr, "fd00:abcd::1"); + + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: Ping from LEADER to the address on ROUTER_1"); + + // Ping from leader the address added on router1. The zero + // length external route should ensure the message is routed + // to router1. + Ip6::Address destAddress; + SuccessOrQuit(destAddress.FromString("fd00:abcd::1")); + nexus.SendAndVerifyEchoRequest(leader, destAddress, kEchoPayloadSize, Ip6::kDefaultHopLimit, kEchoResponseWaitTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Change prefix on ROUTER_2 to have default route flag"); + + // Change the on-mesh prefix on router2 to now also have + // the default route flag. + RemovePrefix(router2, "fd00:1234::/64"); + AddPrefix(router2, "fd00:5678::/64", true); + + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: Ping again from LEADER. External route ::/0 should still be preferred"); + + // Again ping from leader the same address. The explicit + // external route (even with zero length) on router1 should + // still be preferred over the default route flag of the + // on-mesh prefix from router2. + nexus.SendAndVerifyEchoRequest(leader, destAddress, kEchoPayloadSize, Ip6::kDefaultHopLimit, kEchoResponseWaitTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: Remove external route on ROUTER_1 and verify ping failure"); + + // Remove the external route on router1. + RemoveExternalRoute(router1, "::/0"); + + nexus.AdvanceTime(kStabilizationTime); + + // Now the ping should fail since the message would be routed + // to router2 (due to its on-mesh prefix with default route flag). + SendAndVerifyNoEchoResponse(nexus, leader, destAddress, kEchoResponseWaitTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 7: Move address to ROUTER_2 and verify ping success"); + + // Remove the address from router1 and add it on router2 and + // ping it again from leader to verify that the message is + // being routed to router2. + RemoveIpAddress(router1, "fd00:abcd::1"); + AddIpAddress(router2, router2Addr, "fd00:abcd::1"); + + nexus.AdvanceTime(kStabilizationTime); + nexus.SendAndVerifyEchoRequest(leader, destAddress, kEchoPayloadSize, Ip6::kDefaultHopLimit, kEchoResponseWaitTime); + + printf("All tests passed\n"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestZeroLenExternalRoute(); + return 0; +} diff --git a/tests/scripts/thread-cert/test_zero_len_external_route.py b/tests/scripts/thread-cert/test_zero_len_external_route.py deleted file mode 100755 index b48dff8bd..000000000 --- a/tests/scripts/thread-cert/test_zero_len_external_route.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2021, 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 unittest - -import config -import thread_cert - -# Test description: -# -# This test verifies the behavior of the network when a zero length external route prefix "::/0" is added. -# -# Topology: -# -# leader --- router1 -# \ / -# \ / -# router2 -# - -LEADER = 1 -ROUTER1 = 2 -ROUTER2 = 3 - - -class ZeroLengthExternalRoute(thread_cert.TestCase): - USE_MESSAGE_FACTORY = False - SUPPORT_NCP = False - - TOPOLOGY = { - LEADER: { - 'name': 'LEADER', - 'mode': 'rdn', - }, - ROUTER1: { - 'name': 'ROUTER2', - 'mode': 'rdn', - }, - ROUTER2: { - 'name': 'ROUTER2', - 'mode': 'rdn', - }, - } - - def test(self): - leader = self.nodes[LEADER] - router1 = self.nodes[ROUTER1] - router2 = self.nodes[ROUTER2] - - # Start the nodes and form the network. - - leader.start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(leader.get_state(), 'leader') - - router1.start() - router2.start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - self.assertEqual(router1.get_state(), 'router') - self.assertEqual(router2.get_state(), 'router') - - # Add an on-mesh prefix with SLAAC on router2 (without default - # route flag). - - router2.add_prefix('fd00:1234::/64', 'paos') - router2.register_netdata() - - # Add a zero length external route on router1. Also add an IPv6 - # address on router1. - - router1.add_route('::/0', stable=True) - router1.add_ipaddr('fd00:abcd::1') - router1.register_netdata() - self.simulator.go(5) - - # Ping from leader the address added on router1. The zero - # length external route should ensure the message is routed - # to router1. - - self.assertTrue(leader.ping('fd00:abcd::1')) - - # Change the on-mesh prefix on router2 to now also have - # the default route flag. - - router2.remove_prefix('fd00:1234::/64') - router2.add_prefix('fd00:5678::/64', 'paros') - router2.register_netdata() - self.simulator.go(5) - - # Again ping from leader the same address. The explicit - # external route (even with zero length) on router1 should - # still be preferred over the default route flag of the - # on-mesh prefix from router2. - - self.assertTrue(leader.ping('fd00:abcd::1')) - - # Remove the external route on router1. - - router1.remove_route("::/0") - router1.register_netdata() - self.simulator.go(5) - - # Now the ping should fail since the message would be routed - # to router2 (due to its on-mesh prefix with default route - # flag). - - self.assertFalse(leader.ping('fd00:abcd::1')) - - # Remove the address from router1 and add it on router2 and - # ping it again from leader to verify that the message is - # being routed to router2. - - router1.del_ipaddr('fd00:abcd::1') - router2.add_ipaddr('fd00:abcd::1') - self.simulator.go(5) - self.assertTrue(leader.ping('fd00:abcd::1')) - - -if __name__ == '__main__': - unittest.main()