diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 990e2b37c..52b6ac18f 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -387,14 +387,15 @@ ot_nexus_test(inform_previous_parent_on_reattach "cert;nexus") ot_nexus_test(border_admitter "core;nexus") ot_nexus_test(border_agent "core;nexus") ot_nexus_test(border_agent_tracker "core;nexus") -ot_nexus_test(log_override "core;nexus") +ot_nexus_test(dataset_updater "core;nexus") ot_nexus_test(discover_scan "core;nexus") ot_nexus_test(dtls "core;nexus") ot_nexus_test(form_join "core;nexus") -ot_nexus_test(dataset_updater "core;nexus") +ot_nexus_test(ipv6_source_selection "core;nexus") +ot_nexus_test(log_override "core;nexus") ot_nexus_test(mle_blocking_downgrade "core;nexus") -ot_nexus_test(router_downgrade_on_sec_policy_change "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") # Trel diff --git a/tests/nexus/platform/nexus_core.cpp b/tests/nexus/platform/nexus_core.cpp index 6e7938159..6e09158e1 100644 --- a/tests/nexus/platform/nexus_core.cpp +++ b/tests/nexus/platform/nexus_core.cpp @@ -1010,6 +1010,7 @@ Core::IcmpEchoResponseContext::IcmpEchoResponseContext(Node &aNode, uint16_t aId : mNode(aNode) , mIdentifier(aIdentifier) , mResponseReceived(false) + , mExpectedSourceCheck(false) { } @@ -1034,6 +1035,14 @@ void Core::HandleIcmpResponse(void *aContext, Log("Received Echo Reply on Node %u (%s) from %s", context->mNode.GetId(), context->mNode.GetName(), messageInfo->GetPeerAddr().ToString().AsCString()); + + if (context->mExpectedSourceCheck) + { + Log("Verifying source address: Expected %s, Actual %s", context->mExpectedSource.ToString().AsCString(), + messageInfo->GetSockAddr().ToString().AsCString()); + + VerifyOrQuit(messageInfo->GetSockAddr() == context->mExpectedSource); + } } } @@ -1057,5 +1066,29 @@ void Core::SendAndVerifyEchoRequest(Node &aSender, SuccessOrQuit(aSender.Get().UnregisterHandler(icmpHandler)); } +void Core::SendAndVerifyEchoRequest(Node &aSender, + const Ip6::Address &aExpectedSource, + const Ip6::Address &aDestination, + uint16_t aPayloadSize, + uint8_t aHopLimit, + uint32_t aResponseTimeout) +{ + static constexpr uint16_t kIdentifier = 0x1234; + + IcmpEchoResponseContext icmpContext(aSender, kIdentifier); + icmpContext.mExpectedSource = aExpectedSource; + icmpContext.mExpectedSourceCheck = true; + + Ip6::Icmp::Handler icmpHandler(HandleIcmpResponse, &icmpContext); + + SuccessOrQuit(aSender.Get().RegisterHandler(icmpHandler)); + + aSender.SendEchoRequest(aDestination, kIdentifier, aPayloadSize, aHopLimit); + AdvanceTime(aResponseTimeout); + VerifyOrQuit(icmpContext.mResponseReceived); + + SuccessOrQuit(aSender.Get().UnregisterHandler(icmpHandler)); +} + } // namespace Nexus } // namespace ot diff --git a/tests/nexus/platform/nexus_core.hpp b/tests/nexus/platform/nexus_core.hpp index 176b9ddbf..40309038e 100644 --- a/tests/nexus/platform/nexus_core.hpp +++ b/tests/nexus/platform/nexus_core.hpp @@ -91,6 +91,13 @@ public: uint8_t aHopLimit = Ip6::kDefaultHopLimit, uint32_t aResponseTimeout = 1000); + void SendAndVerifyEchoRequest(Node &aSender, + const Ip6::Address &aExpectedSource, + const Ip6::Address &aDestination, + uint16_t aPayloadSize = 0, + uint8_t aHopLimit = Ip6::kDefaultHopLimit, + uint32_t aResponseTimeout = 1000); + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Used by platform implementation @@ -121,9 +128,11 @@ private: { IcmpEchoResponseContext(Node &aNode, uint16_t aIdentifier); - Node &mNode; - uint16_t mIdentifier; - bool mResponseReceived; + Node &mNode; + uint16_t mIdentifier; + bool mResponseReceived; + Ip6::Address mExpectedSource; + bool mExpectedSourceCheck; }; struct TestVar diff --git a/tests/nexus/test_ipv6_source_selection.cpp b/tests/nexus/test_ipv6_source_selection.cpp new file mode 100644 index 000000000..eee1210bb --- /dev/null +++ b/tests/nexus/test_ipv6_source_selection.cpp @@ -0,0 +1,153 @@ +/* + * 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 { + +void TestIPv6SourceSelection(void) +{ + /** + * Test IPv6 Source Address Selection + * + * Purpose & Description: + * This test verifies that the correct source IPv6 address is selected based on the destination address. + */ + + Core nexus; + + Node &leader = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + + leader.SetName("LEADER"); + router.SetName("ROUTER"); + + AllowLinkBetween(leader, router); + + nexus.AdvanceTime(0); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 0: Form network"); + + leader.Form(); + nexus.AdvanceTime(13 * 1000); // kFormNetworkTime + VerifyOrQuit(leader.Get().IsLeader()); + + router.Join(leader); + nexus.AdvanceTime(200 * 1000); // kAttachToRouterTime + VerifyOrQuit(router.Get().IsRouter()); + + Ip6::Address leaderRloc = leader.Get().GetMeshLocalRloc(); + Ip6::Address leaderMleid = leader.Get().GetMeshLocalEid(); + Ip6::Address leaderLinkLocal = leader.Get().GetLinkLocalAddress(); + Ip6::Address leaderAloc; + leader.Get().GetLeaderAloc(leaderAloc); + + Ip6::Address routerRloc = router.Get().GetMeshLocalRloc(); + Ip6::Address routerMleid = router.Get().GetMeshLocalEid(); + Ip6::Address routerLinkLocal = router.Get().GetLinkLocalAddress(); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: RLOC source for RLOC destination"); + nexus.SendAndVerifyEchoRequest(router, routerRloc, leaderRloc); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: ML-EID source for ALOC destination"); + nexus.SendAndVerifyEchoRequest(router, routerMleid, leaderAloc); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: ML-EID source for ML-EID destination"); + nexus.SendAndVerifyEchoRequest(router, routerMleid, leaderMleid); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Link-local source for Link-local destination"); + nexus.SendAndVerifyEchoRequest(router, routerLinkLocal, leaderLinkLocal); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: ML-EID source for Realm-local multicast destination"); + Ip6::Address multicastAddr; + SuccessOrQuit(multicastAddr.FromString("ff03::1")); + nexus.SendAndVerifyEchoRequest(router, routerMleid, multicastAddr); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: GUA source for GUA destination"); + + NetworkData::OnMeshPrefixConfig config; + config.Clear(); + SuccessOrQuit(config.GetPrefix().FromString("2001::/64")); + config.mPreferred = true; + config.mSlaac = true; + config.mOnMesh = true; + config.mDefaultRoute = true; + config.mStable = true; + + SuccessOrQuit(leader.Get().AddOnMeshPrefix(config)); + leader.Get().HandleServerDataUpdated(); + nexus.AdvanceTime(10 * 1000); + + const Ip6::Address &leaderGua = leader.FindMatchingAddress("2001::/64"); + const Ip6::Address &routerGua = router.FindMatchingAddress("2001::/64"); + + VerifyOrQuit(!leaderGua.IsUnspecified()); + VerifyOrQuit(!routerGua.IsUnspecified()); + + nexus.SendAndVerifyEchoRequest(router, routerGua, leaderGua); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 7: GUA source for external address (default route)"); + + Ip6::Address externalAddr; + SuccessOrQuit(externalAddr.FromString("2007::1")); + + // Add externalAddr to leader so it responds to pings + otNetifAddress extNetifAddr; + memset(&extNetifAddr, 0, sizeof(extNetifAddr)); + AsCoreType(&extNetifAddr.mAddress) = externalAddr; + extNetifAddr.mPrefixLength = 128; + extNetifAddr.mPreferred = true; + extNetifAddr.mValid = true; + SuccessOrQuit(otIp6AddUnicastAddress(&leader.GetInstance(), &extNetifAddr)); + + nexus.SendAndVerifyEchoRequest(router, routerGua, externalAddr); + + nexus.SaveTestInfo("test_ipv6_source_selection.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestIPv6SourceSelection(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/thread-cert/test_ipv6_source_selection.py b/tests/scripts/thread-cert/test_ipv6_source_selection.py deleted file mode 100755 index 0985e23bd..000000000 --- a/tests/scripts/thread-cert/test_ipv6_source_selection.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2019, 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 ipv6 -import thread_cert - -LEADER = 1 -ROUTER = 2 - - -class TestIPv6SourceSelection(thread_cert.TestCase): - SUPPORT_NCP = False - - TOPOLOGY = { - LEADER: { - 'mode': 'rdn', - 'panid': 0xcafe, - 'allowlist': [ROUTER] - }, - ROUTER: { - 'mode': 'rdn', - 'panid': 0xcafe, - 'allowlist': [LEADER] - }, - } - - def test(self): - self.nodes[LEADER].start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(self.nodes[LEADER].get_state(), 'leader') - - self.nodes[ROUTER].start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - self.assertEqual(self.nodes[ROUTER].get_state(), 'router') - - leader_aloc = self.nodes[LEADER].get_addr_leader_aloc() - leader_mleid = self.nodes[LEADER].get_mleid() - leader_rloc = self.nodes[LEADER].get_rloc() - leader_linklocal = self.nodes[LEADER].get_linklocal() - multicast_addr = 'ff03::1' - external_addr = '2007::1' - - router_rloc = self.nodes[ROUTER].get_rloc() - router_linklocal = self.nodes[ROUTER].get_linklocal() - router_mleid = self.nodes[ROUTER].get_mleid() - - # Source check - RLOC source for RLOC destination - self.assertTrue(self.nodes[ROUTER].ping(leader_rloc)) - router_msgs = self.simulator.get_messages_sent_by(ROUTER) - msg = router_msgs.get_icmp_message(ipv6.ICMP_ECHO_REQUEST) - msg.assertSentFromSourceAddress(router_rloc) - - # Source check - ML-EID source for ALOC destination - self.assertTrue(self.nodes[ROUTER].ping(leader_aloc)) - router_msgs = self.simulator.get_messages_sent_by(ROUTER) - msg = router_msgs.get_icmp_message(ipv6.ICMP_ECHO_REQUEST) - msg.assertSentFromSourceAddress(router_mleid) - - # Source check - ML-EID source for ML-EID destination - self.assertTrue(self.nodes[ROUTER].ping(leader_mleid)) - router_msgs = self.simulator.get_messages_sent_by(ROUTER) - msg = router_msgs.get_icmp_message(ipv6.ICMP_ECHO_REQUEST) - msg.assertSentFromSourceAddress(router_mleid) - - # Source check - link local source source for link local address - self.assertTrue(self.nodes[ROUTER].ping(leader_linklocal)) - router_msgs = self.simulator.get_messages_sent_by(ROUTER) - msg = router_msgs.get_icmp_message(ipv6.ICMP_ECHO_REQUEST) - msg.assertSentFromSourceAddress(router_linklocal) - - # Source check - ML-EID source for realmlocal multicast destination - self.assertTrue(self.nodes[ROUTER].ping(multicast_addr)) - router_msgs = self.simulator.get_messages_sent_by(ROUTER) - msg = router_msgs.get_icmp_message(ipv6.ICMP_ECHO_REQUEST) - msg.assertSentFromSourceAddress(router_mleid) - - # GUA and default gateway - self.nodes[LEADER].add_prefix('2001::/64', 'paros') - self.nodes[LEADER].register_netdata() - self.simulator.go(5) - - # Set lowpan context of sniffer - self.simulator.set_lowpan_context(1, '2001::/64') - - # Flushes message queue before next ping - router_msgs = self.simulator.get_messages_sent_by(ROUTER) - - # Source check - GUA source for GUA destination - leader_gua = self.nodes[LEADER].get_addr("2001::/64") - router_gua = self.nodes[ROUTER].get_addr("2001::/64") - self.assertTrue(leader_gua is not None) - self.assertTrue(router_gua is not None) - self.assertTrue(self.nodes[ROUTER].ping(leader_gua)) - router_msgs = self.simulator.get_messages_sent_by(ROUTER) - msg = router_msgs.get_icmp_message(ipv6.ICMP_ECHO_REQUEST) - msg.assertSentFromSourceAddress(router_gua) - - # Source check - GUA source for external address (default route) - self.nodes[ROUTER].ping(external_addr) - router_msgs = self.simulator.get_messages_sent_by(ROUTER) - msg = router_msgs.get_icmp_message(ipv6.ICMP_ECHO_REQUEST) - msg.assertSentFromSourceAddress(router_gua) - - -if __name__ == '__main__': - unittest.main()