diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 9904ec2e8..80e5f3f42 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -44,6 +44,7 @@ set(COMMON_COMPILE_OPTIONS add_library(ot-nexus-platform platform/nexus_alarm.cpp platform/nexus_core.cpp + platform/nexus_dns.cpp platform/nexus_infra_if.cpp platform/nexus_logging.cpp platform/nexus_mdns.cpp @@ -293,6 +294,7 @@ ot_nexus_test(1_4_TREL_TC_4 "cert;nexus") ot_nexus_test(1_4_TREL_TC_5 "cert;nexus") ot_nexus_test(1_4_TREL_TC_6 "cert;nexus") ot_nexus_test(1_4_DNS_TC_1 "cert;nexus") +ot_nexus_test(1_4_DNS_TC_3 "cert;nexus") ot_nexus_test(1_4_CS_TC_3 "cert;nexus") # Misc tests diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index 2728b117c..ffe366a7f 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -55,6 +55,7 @@ #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE 0 #define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 1 +#define OPENTHREAD_CONFIG_BORDER_ROUTING_TESTING_API_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE 1 #define OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE 1 #define OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE 1 @@ -70,6 +71,7 @@ #define OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE 1 #define OPENTHREAD_CONFIG_DNS_CLIENT_BIND_UDP_TO_THREAD_NETIF 1 #define OPENTHREAD_CONFIG_DNS_DSO_ENABLE 0 +#define OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE 1 #define OPENTHREAD_CONFIG_DNSSD_DISCOVERY_PROXY_ENABLE 1 #define OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE 1 #define OPENTHREAD_CONFIG_ECDSA_ENABLE 1 @@ -120,6 +122,7 @@ #define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION "OT-simul-nexus" #define OPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE 1 #define OPENTHREAD_CONFIG_NUM_MESSAGE_BUFFERS 256 +#define OPENTHREAD_CONFIG_PLATFORM_DNS_ENABLE 1 #define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION 0 #define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE 0 #ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION @@ -157,6 +160,4 @@ #define OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE 1 #define OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL OT_LOG_LEVEL_INFO -#define OPENTHREAD_CONFIG_BORDER_ROUTING_TESTING_API_ENABLE 1 - #endif // OT_NEXUS_OPENTHREAD_CORE_NEXUS_CONFIG_H_ diff --git a/tests/nexus/platform/nexus_core.cpp b/tests/nexus/platform/nexus_core.cpp index 99d5fbb3d..5bb923ea3 100644 --- a/tests/nexus/platform/nexus_core.cpp +++ b/tests/nexus/platform/nexus_core.cpp @@ -189,6 +189,29 @@ void Core::SaveTestInfo(const char *aFilename, Node *aLeaderNode) } fprintf(file, " },\n"); + fprintf(file, " \"ipaddrs\": {\n"); + for (Node &node : mNodes) + { + bool first = true; + + fprintf(file, " \"%u\": [\n", node.GetInstance().GetId()); + + for (const Ip6::Netif::UnicastAddress &addr : node.Get().GetUnicastAddresses()) + { + fprintf(file, "%s \"%s\"", first ? "" : ",\n", addr.GetAddress().ToString().AsCString()); + first = false; + } + + for (const Ip6::Address &infraAddr : node.mInfraIf.GetAddresses()) + { + fprintf(file, "%s \"%s\"", first ? "" : ",\n", infraAddr.ToString().AsCString()); + first = false; + } + + fprintf(file, "\n ]%s\n", (&node == tail) ? "" : ","); + } + fprintf(file, " },\n"); + fprintf(file, " \"ethaddrs\": {\n"); for (Node &node : mNodes) { @@ -244,25 +267,6 @@ void Core::SaveTestInfo(const char *aFilename, Node *aLeaderNode) } fprintf(file, " },\n"); - fprintf(file, " \"ipaddrs\": {\n"); - for (Node &node : mNodes) - { - bool first = true; - - fprintf(file, " \"%u\": [\n", node.GetInstance().GetId()); - for (const Ip6::Netif::UnicastAddress &addr : node.Get().GetUnicastAddresses()) - { - if (!first) - { - fprintf(file, ",\n"); - } - fprintf(file, " \"%s\"", addr.GetAddress().ToString().AsCString()); - first = false; - } - fprintf(file, "\n ]%s\n", (&node == tail) ? "" : ","); - } - fprintf(file, " },\n"); - fprintf(file, " \"extra_vars\": {\n"); if (leaderNode != nullptr) { diff --git a/tests/nexus/platform/nexus_dns.cpp b/tests/nexus/platform/nexus_dns.cpp new file mode 100644 index 000000000..604858b4f --- /dev/null +++ b/tests/nexus/platform/nexus_dns.cpp @@ -0,0 +1,213 @@ +/* + * 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. + */ + +/** + * @file + * This file implements the platform DNS APIs for Nexus. + */ + +#include "nexus_dns.hpp" + +#include + +#include "nexus_core.hpp" +#include "nexus_node.hpp" +#include "border_router/rx_ra_tracker.hpp" +#include "common/as_core_type.hpp" +#include "common/code_utils.hpp" +#include "common/debug.hpp" +#include "net/dns_types.hpp" + +namespace ot { +namespace Nexus { + +bool UpstreamDns::IsGua(const Ip6::Prefix &aPrefix) const { return !aPrefix.IsLinkLocal() && !aPrefix.IsUniqueLocal(); } + +uint16_t UpstreamDns::CountGuaPrefixes(const Ip6::Address &aRouterAddress) const +{ + const BorderRouter::RxRaTracker &rxRaTracker = Get(); + BorderRouter::PrefixTableIterator iter; + BorderRouter::PrefixTableEntry entry; + uint16_t count = 0; + + rxRaTracker.InitIterator(iter); + while (rxRaTracker.GetNextPrefixTableEntry(iter, entry) == kErrorNone) + { + if (entry.mValidLifetime > 0 && IsGua(AsCoreType(&entry.mPrefix)) && + AsCoreType(&entry.mRouter.mAddress) == aRouterAddress) + { + count++; + } + } + + return count; +} + +Error UpstreamDns::SelectUpstreamDnsServer(Ip6::Address &aSelectedAddress) const +{ + const BorderRouter::RxRaTracker &rxRaTracker = Get(); + BorderRouter::PrefixTableIterator iter; + BorderRouter::RdnssAddrEntry entry; + bool found = false; + bool bestHasGua = false; + + rxRaTracker.InitIterator(iter); + while (rxRaTracker.GetNextRdnssAddrEntry(iter, entry) == kErrorNone) + { + if (!entry.mRouter.mIsReachable) + { + continue; + } + + bool hasGua = (CountGuaPrefixes(AsCoreType(&entry.mRouter.mAddress)) > 0); + + if (!found || (hasGua && !bestHasGua)) + { + aSelectedAddress = AsCoreType(&entry.mAddress); + bestHasGua = hasGua; + found = true; + } + } + + return found ? kErrorNone : kErrorNotFound; +} + +UpstreamDns::UpstreamDns(Instance &aInstance) + : InstanceLocator(aInstance) +{ +} + +void UpstreamDns::StartUpstreamQuery(UpstreamQueryTransaction &aTxn, const Message &aQuery) +{ + Error error = kErrorNone; + Node &node = AsNode(&GetInstance()); + Ip6::Address serverAddress; + ot::Dns::Header dnsHeader; + PendingQuery *pendingQuery; + Message *message = nullptr; + // We use kDnsPort (53) as both source and destination port for upstream queries to simplify interception + // and response matching in this simulation environment. + + SuccessOrExit(error = SelectUpstreamDnsServer(serverAddress)); + + SuccessOrExit(error = aQuery.Read(0, dnsHeader)); + + message = node.Get().CloneMessage(aQuery); + VerifyOrExit(message != nullptr, error = kErrorNoBufs); + + pendingQuery = PendingQuery::Allocate(); + VerifyOrExit(pendingQuery != nullptr, error = kErrorNoBufs); + + pendingQuery->mTxn = &aTxn; + pendingQuery->mMessageId = dnsHeader.GetMessageId(); + pendingQuery->mServerAddress = serverAddress; + mPendingQueries.Push(*pendingQuery); + + node.mInfraIf.SendUdp(node.mInfraIf.SelectSourceAddress(serverAddress), serverAddress, kDnsPort, kDnsPort, + *message); + message = nullptr; + +exit: + if (error != kErrorNone) + { + if (message != nullptr) + { + message->Free(); + } + otPlatDnsUpstreamQueryDone(&GetInstance(), &aTxn, nullptr); + } +} + +void UpstreamDns::CancelUpstreamQuery(UpstreamQueryTransaction &aTxn) +{ + mPendingQueries.RemoveAndFreeAllMatching(&aTxn); +} + +void UpstreamDns::Reset(void) { mPendingQueries.Clear(); } + +bool UpstreamDns::IsUpstreamQueryAvailable(void) const +{ + Ip6::Address address; + return SelectUpstreamDnsServer(address) == kErrorNone; +} + +bool UpstreamDns::HandleUpstreamDnsResponse(const Ip6::Address &aSrcAddress, Message &aMessage) +{ + Instance &instance = GetInstance(); + Node &node = AsNode(&instance); + ot::Dns::Header dnsHeader; + OwnedPtr query; + bool handled = false; + + SuccessOrExit(aMessage.Read(aMessage.GetOffset(), dnsHeader)); + + query = mPendingQueries.RemoveMatching(dnsHeader.GetMessageId(), aSrcAddress); + + if (query != nullptr) + { + // Found matching query + // We use kTypeOther as it is appropriate for a payload-only message. + Message *response = node.Get().Allocate(Message::kTypeOther); + + if (response != nullptr) + { + if (response->AppendBytesFromMessage(aMessage, aMessage.GetOffset(), + aMessage.GetLength() - aMessage.GetOffset()) != kErrorNone) + { + response->Free(); + response = nullptr; + } + } + + otPlatDnsUpstreamQueryDone(&instance, query->mTxn, response); + handled = true; + } + +exit: + return handled; +} + +extern "C" bool otPlatDnsIsUpstreamQueryAvailable(otInstance *aInstance) +{ + return AsNode(aInstance).mUpstreamDns.IsUpstreamQueryAvailable(); +} + +extern "C" void otPlatDnsStartUpstreamQuery(otInstance *aInstance, + otPlatDnsUpstreamQuery *aTxn, + const otMessage *aQuery) +{ + AsNode(aInstance).mUpstreamDns.StartUpstreamQuery(AsCoreType(aTxn), AsCoreType(aQuery)); +} + +extern "C" void otPlatDnsCancelUpstreamQuery(otInstance *aInstance, otPlatDnsUpstreamQuery *aTxn) +{ + AsNode(aInstance).mUpstreamDns.CancelUpstreamQuery(AsCoreType(aTxn)); +} + +} // namespace Nexus +} // namespace ot diff --git a/tests/nexus/platform/nexus_dns.hpp b/tests/nexus/platform/nexus_dns.hpp new file mode 100644 index 000000000..93a30bf49 --- /dev/null +++ b/tests/nexus/platform/nexus_dns.hpp @@ -0,0 +1,87 @@ +/* + * 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. + */ + +#ifndef OT_NEXUS_PLATFORM_NEXUS_DNS_HPP_ +#define OT_NEXUS_PLATFORM_NEXUS_DNS_HPP_ + +#include +#include "common/heap_allocatable.hpp" +#include "common/linked_list.hpp" +#include "common/locator.hpp" +#include "common/owning_list.hpp" +#include "instance/instance.hpp" +#include "net/dnssd_server.hpp" +#include "net/ip6_address.hpp" + +namespace ot { +namespace Nexus { + +class Node; + +class UpstreamDns : public InstanceLocator +{ +public: + static constexpr uint16_t kDnsPort = 53; + + typedef Dns::ServiceDiscovery::Server::UpstreamQueryTransaction UpstreamQueryTransaction; + + struct PendingQuery : public Heap::Allocatable, public LinkedListEntry + { + bool Matches(const UpstreamQueryTransaction *aTxn) const { return mTxn == aTxn; } + bool Matches(uint16_t aMessageId, const Ip6::Address &aSrcAddr) const + { + return (mMessageId == aMessageId) && (mServerAddress == aSrcAddr); + } + + UpstreamQueryTransaction *mTxn; + uint16_t mMessageId; + Ip6::Address mServerAddress; + PendingQuery *mNext; + }; + + explicit UpstreamDns(Instance &aInstance); + ~UpstreamDns(void) { Reset(); } + + void StartUpstreamQuery(UpstreamQueryTransaction &aTxn, const Message &aQuery); + void CancelUpstreamQuery(UpstreamQueryTransaction &aTxn); + bool HandleUpstreamDnsResponse(const Ip6::Address &aSrcAddress, Message &aMessage); + bool IsUpstreamQueryAvailable(void) const; + void Reset(void); + +private: + bool IsGua(const Ip6::Prefix &aPrefix) const; + uint16_t CountGuaPrefixes(const Ip6::Address &aRouterAddress) const; + Error SelectUpstreamDnsServer(Ip6::Address &aSelectedAddress) const; + + OwningList mPendingQueries; +}; + +} // namespace Nexus +} // namespace ot + +#endif // OT_NEXUS_PLATFORM_NEXUS_DNS_HPP_ diff --git a/tests/nexus/platform/nexus_infra_if.cpp b/tests/nexus/platform/nexus_infra_if.cpp index ea3c2dab1..9edcaa16d 100644 --- a/tests/nexus/platform/nexus_infra_if.cpp +++ b/tests/nexus/platform/nexus_infra_if.cpp @@ -38,6 +38,7 @@ InfraIf::InfraIf(Instance &aInstance) : mNode(nullptr) , mNodeId(0) , mIfIndex(0) + , mUdpHook(nullptr) , mHasRioPrefix(false) , mRaTimer(aInstance) { @@ -122,6 +123,34 @@ const Ip6::Address &InfraIf::FindMatchingAddress(const char *aPrefix) const return *matchedAddress; } +const Ip6::Address &InfraIf::SelectSourceAddress(const Ip6::Address &aDestination) const +{ + const Ip6::Address *bestMatch = &GetLinkLocalAddress(); + + if (aDestination.IsLinkLocalUnicast()) + { + // For link-local destinations, we always use the link-local address. + ExitNow(); + } + + for (const Ip6::Address &address : mAddresses) + { + if (address.IsMulticast()) + { + continue; + } + + if (address.GetScope() == aDestination.GetScope()) + { + bestMatch = &address; + break; + } + } + +exit: + return *bestMatch; +} + void InfraIf::SendIcmp6Nd(const Ip6::Address &aDestAddress, const uint8_t *aBuffer, uint16_t aBufferLength) { Message *message = GetNode().Get().Allocate(Message::kTypeIp6); @@ -340,20 +369,6 @@ void InfraIf::SendEchoRequest(const Ip6::Address &aSrcAddress, mPendingTxQueue.Enqueue(*message); } -void InfraIf::SendUdp(const Ip6::Address &aSrcAddress, - const Ip6::Address &aDestAddress, - uint16_t aSourcePort, - uint16_t aDestPort, - uint16_t aPayloadSize) -{ - Message *message = GetNode().Get().NewMessage(); - - VerifyOrQuit(message != nullptr); - - SuccessOrQuit(message->IncreaseLength(aPayloadSize)); - SendUdp(aSrcAddress, aDestAddress, aSourcePort, aDestPort, *message); -} - void InfraIf::SendUdp(const Ip6::Address &aSrcAddress, const Ip6::Address &aDestAddress, uint16_t aSourcePort, @@ -418,9 +433,8 @@ void InfraIf::Receive(Message &aMessage) SuccessOrQuit(payload.SetFrom(aMessage, offset, aMessage.GetLength() - offset)); - otPlatInfraIfRecvIcmp6Nd(&node.GetInstance(), mIfIndex, - reinterpret_cast(&headers.GetSourceAddress()), - payload.GetBytes(), payload.GetLength()); + otPlatInfraIfRecvIcmp6Nd(&node.GetInstance(), mIfIndex, &headers.GetSourceAddress(), payload.GetBytes(), + payload.GetLength()); node.mInfraIf.ProcessIcmp6Nd(headers.GetSourceAddress(), payload.GetBytes(), payload.GetLength()); ExitNow(); } @@ -479,11 +493,39 @@ void InfraIf::Receive(Message &aMessage) } #endif +#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE + if (headers.IsUdp() && headers.GetDestinationPort() == UpstreamDns::kDnsPort) + { + aMessage.SetOffset(sizeof(Ip6::Header) + sizeof(Ip6::Udp::Header)); + if (node.mUpstreamDns.HandleUpstreamDnsResponse(headers.GetSourceAddress(), aMessage)) + { + ExitNow(); + } + aMessage.SetOffset(0); + } +#endif + if (headers.IsUdp() && node.mUdp.HandleReceive(aMessage, headers)) { ExitNow(); } + if (headers.IsUdp() && mUdpHook != nullptr) + { + Ip6::MessageInfo messageInfo; + messageInfo.SetPeerAddr(headers.GetSourceAddress()); + messageInfo.SetPeerPort(headers.GetSourcePort()); + messageInfo.SetSockAddr(headers.GetDestinationAddress()); + messageInfo.SetSockPort(headers.GetDestinationPort()); + + aMessage.SetOffset(sizeof(Ip6::Header) + sizeof(Ip6::Udp::Header)); + if (mUdpHook(node.GetInstance(), aMessage, messageInfo)) + { + ExitNow(); + } + aMessage.SetOffset(0); + } + { // We also deliver generic IPv6 packets to the stack if they are NOT ICMPv6 ND packets. // (ND packets were already delivered via otPlatInfraIfRecvIcmp6Nd above). diff --git a/tests/nexus/platform/nexus_infra_if.hpp b/tests/nexus/platform/nexus_infra_if.hpp index 5e620bdc4..cf4336fe3 100644 --- a/tests/nexus/platform/nexus_infra_if.hpp +++ b/tests/nexus/platform/nexus_infra_if.hpp @@ -58,6 +58,8 @@ public: const Ip6::Address &GetLinkLocalAddress(void) const { return mAddresses[0]; } const Heap::Array &GetAddresses(void) const { return mAddresses; } + const Ip6::Address &SelectSourceAddress(const Ip6::Address &aDestination) const; + void SendIcmp6Nd(const Ip6::Address &aDestAddress, const uint8_t *aBuffer, uint16_t aBufferLength); void SendRouterAdvertisement(const Ip6::Address &aDestination, const Ip6::Prefix *aPioPrefix, @@ -70,11 +72,6 @@ public: uint16_t aIdentifier, uint16_t aPayloadSize, uint8_t aHopLimit = Ip6::kDefaultHopLimit); - void SendUdp(const Ip6::Address &aSrcAddress, - const Ip6::Address &aDestAddress, - uint16_t aSourcePort, - uint16_t aDestPort, - uint16_t aPayloadSize); void SendUdp(const Ip6::Address &aSrcAddress, const Ip6::Address &aDestAddress, uint16_t aSourcePort, @@ -88,6 +85,9 @@ public: void SetEchoReplyHandler(EchoReplyHandler aHandler, void *aContext) { mEchoReplyCallback.Set(aHandler, aContext); } + typedef bool (*UdpHook)(Instance &aInstance, Message &aMessage, const Ip6::MessageInfo &aMessageInfo); + void SetUdpHook(UdpHook aHook) { mUdpHook = aHook; } + Node &GetNode(void); const Node &GetNode(void) const; @@ -108,6 +108,7 @@ private: uint32_t mIfIndex; Heap::Array mAddresses; Callback mEchoReplyCallback; + UdpHook mUdpHook; Ip6::Prefix mPioPrefix; Ip6::Prefix mRioPrefix; diff --git a/tests/nexus/platform/nexus_node.cpp b/tests/nexus/platform/nexus_node.cpp index a4eb8fa1f..db593f42c 100644 --- a/tests/nexus/platform/nexus_node.cpp +++ b/tests/nexus/platform/nexus_node.cpp @@ -40,6 +40,7 @@ void Node::Reset(void) mAlarmMilli.Reset(); mAlarmMicro.Reset(); mMdns.Reset(); + mUpstreamDns.Reset(); mInfraIf.mPendingTxQueue.DequeueAndFreeAll(); mPendingTasklet = false; diff --git a/tests/nexus/platform/nexus_node.hpp b/tests/nexus/platform/nexus_node.hpp index da39e2183..575e10270 100644 --- a/tests/nexus/platform/nexus_node.hpp +++ b/tests/nexus/platform/nexus_node.hpp @@ -33,6 +33,7 @@ #include "nexus_alarm.hpp" #include "nexus_core.hpp" +#include "nexus_dns.hpp" #include "nexus_infra_if.hpp" #include "nexus_logging.hpp" #include "nexus_mdns.hpp" @@ -48,14 +49,15 @@ namespace Nexus { class Platform { public: - Radio mRadio; - Alarm mAlarmMilli; - Alarm mAlarmMicro; - Logging mLogging; - Mdns mMdns; - InfraIf mInfraIf; - Udp mUdp; - Settings mSettings; + Radio mRadio; + Alarm mAlarmMilli; + Alarm mAlarmMicro; + Logging mLogging; + Mdns mMdns; + UpstreamDns mUpstreamDns; + InfraIf mInfraIf; + Udp mUdp; + Settings mSettings; #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE Trel mTrel; #endif @@ -63,7 +65,8 @@ public: protected: explicit Platform(Instance &aInstance) - : mInfraIf(aInstance) + : mUpstreamDns(aInstance) + , mInfraIf(aInstance) , mUdp(aInstance) , mPendingTasklet(false) { @@ -155,6 +158,7 @@ public: using Platform::mRadio; using Platform::mSettings; using Platform::mUdp; + using Platform::mUpstreamDns; #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE using Platform::mTrel; #endif diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index 6d4f65f4c..be039e851 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -227,6 +227,7 @@ DEFAULT_TESTS=( "1_4_TREL_TC_5" "1_4_TREL_TC_6" "1_4_DNS_TC_1" + "1_4_DNS_TC_3" "1_4_CS_TC_3" ) diff --git a/tests/nexus/test_1_2_MATN_TC_5.cpp b/tests/nexus/test_1_2_MATN_TC_5.cpp index 95fdde9ff..a58f46efc 100644 --- a/tests/nexus/test_1_2_MATN_TC_5.cpp +++ b/tests/nexus/test_1_2_MATN_TC_5.cpp @@ -262,7 +262,13 @@ void TestMatnTc5(void) * - N/A */ Log("Step 5: Host sends a UDP packet to the multicast address, MA1, port 5683."); - host.mInfraIf.SendUdp(*hostUla, ma1, kCoapPort, kCoapPort, kUdpPayloadSize); + { + Message *message = host.Get().NewMessage(); + + VerifyOrQuit(message != nullptr); + SuccessOrQuit(message->SetLength(kUdpPayloadSize)); + host.mInfraIf.SendUdp(*hostUla, ma1, kCoapPort, kCoapPort, *message); + } nexus.AdvanceTime(0); /** diff --git a/tests/nexus/test_1_4_DNS_TC_3.cpp b/tests/nexus/test_1_4_DNS_TC_3.cpp new file mode 100644 index 000000000..9dfeffa66 --- /dev/null +++ b/tests/nexus/test_1_4_DNS_TC_3.cpp @@ -0,0 +1,475 @@ +/* + * 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 "nexus_core.hpp" +#include "nexus_node.hpp" + +namespace ot { +namespace Nexus { + +namespace { + +static constexpr char kGua1Prefix[] = "2005:1234:abcd:100::/64"; + +static constexpr char kEth1Addr[] = "2005:1234:abcd:100::E1"; +static constexpr char kEth2Addr[] = "2005:1234:abcd:100::E2"; + +static constexpr char kThreadGroup1[] = "threadgroup1.org"; +static constexpr char kThreadGroup2[] = "threadgroup2.org"; +static constexpr char kThreadGroup3[] = "threadgroup3.org"; + +static constexpr char kEth1Answer1[] = "2002:1234::E1:1"; +static constexpr char kEth1Answer2[] = "2002:1234::E1:2"; +static constexpr char kEth1Answer3[] = "2002:1234::E1:3"; + +static constexpr char kEth2Answer1[] = "2002:1234::E2:1"; +static constexpr char kEth2Answer2[] = "2002:1234::E2:2"; +static constexpr char kEth2Answer3[] = "2002:1234::E2:3"; + +struct Record +{ + const char *mName; + const char *mAddress; +}; + +const Record kEth1Records[] = { + {kThreadGroup1, kEth1Answer1}, + {kThreadGroup2, kEth1Answer2}, + {kThreadGroup3, kEth1Answer3}, +}; + +const Record kEth2Records[] = { + {kThreadGroup1, kEth2Answer1}, + {kThreadGroup2, kEth2Answer2}, + {kThreadGroup3, kEth2Answer3}, +}; + +static Node *sEth1Node = nullptr; +static Node *sEth2Node = nullptr; + +bool HandleUdpHook(Instance &aInstance, Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + Node &node = AsNode(&aInstance); + const Record *records = nullptr; + uint16_t count = 0; + Dns::Header dnsHeader; + Dns::Question question; + char name[Dns::Name::kMaxNameSize]; + uint16_t readPos; + Message *response; + bool handled = false; + + if (&node == sEth1Node) + { + records = kEth1Records; + count = GetArrayLength(kEth1Records); + } + else if (&node == sEth2Node) + { + records = kEth2Records; + count = GetArrayLength(kEth2Records); + } + + VerifyOrExit(records != nullptr); + VerifyOrExit(aMessageInfo.GetSockPort() == UpstreamDns::kDnsPort); + + SuccessOrExit(aMessage.Read(aMessage.GetOffset(), dnsHeader)); + VerifyOrExit(dnsHeader.GetType() == Dns::Header::kTypeQuery); + VerifyOrExit(dnsHeader.GetQuestionCount() == 1); + + readPos = aMessage.GetOffset() + sizeof(dnsHeader); + SuccessOrExit(Dns::Name::ReadName(aMessage, readPos, name)); + SuccessOrExit(aMessage.Read(readPos, question)); + + Log("Node %u (%s) received DNS query for %s", node.GetInstance().GetId(), node.GetName(), name); + + response = node.Get().Allocate(Message::kTypeIp6); + VerifyOrExit(response != nullptr); + + dnsHeader.SetType(Dns::Header::kTypeResponse); + dnsHeader.SetAnswerCount(0); + dnsHeader.SetRecursionDesiredFlag(); + dnsHeader.SetRecursionAvailableFlag(); + + for (uint16_t i = 0; i < count; i++) + { + if (StringMatch(name, records[i].mName, kStringCaseInsensitiveMatch)) + { + Ip6::Address address; + Dns::ResourceRecord rr; + + SuccessOrQuit(address.FromString(records[i].mAddress)); + + dnsHeader.SetAnswerCount(1); + SuccessOrQuit(response->Append(dnsHeader)); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + SuccessOrQuit(response->Append(question)); + + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + rr.Init(Dns::ResourceRecord::kTypeAaaa); + rr.SetTtl(1800); + rr.SetLength(sizeof(Ip6::Address)); + SuccessOrQuit(response->Append(rr)); + SuccessOrQuit(response->Append(address)); + + Log("Node %u (%s) sending DNS response for %s with address %s", node.GetInstance().GetId(), node.GetName(), + name, records[i].mAddress); + break; + } + } + + if (dnsHeader.GetAnswerCount() == 0) + { + dnsHeader.SetResponseCode(Dns::Header::kResponseNameError); + SuccessOrQuit(response->Append(dnsHeader)); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + SuccessOrQuit(response->Append(question)); + Log("Node %u (%s) sending DNS NXDOMAIN for %s", node.GetInstance().GetId(), node.GetName(), name); + } + + node.mInfraIf.SendUdp(aMessageInfo.GetSockAddr(), aMessageInfo.GetPeerAddr(), UpstreamDns::kDnsPort, + aMessageInfo.GetPeerPort(), *response); + handled = true; + +exit: + return handled; +} + +void HandleDnsResponse(otError aError, const otDnsAddressResponse *aResponse, void *aContext) +{ + OT_UNUSED_VARIABLE(aError); + OT_UNUSED_VARIABLE(aResponse); + OT_UNUSED_VARIABLE(aContext); +} + +static void SendRa(Node &aNode, const Ip6::Prefix &aPrefix, const Ip6::Address &aDnsAddr) +{ + Ip6::Nd::RouterAdvert::TxMessage ra; + Ip6::Nd::RouterAdvert::Header header; + Ip6::Nd::Icmp6Packet packet; + + header.SetToDefault(); + header.SetRouterLifetime(1800); + SuccessOrQuit(ra.Append(header)); + SuccessOrQuit(ra.AppendPrefixInfoOption( + aPrefix, 1800, 1800, Ip6::Nd::PrefixInfoOption::kOnLinkFlag | Ip6::Nd::PrefixInfoOption::kAutoConfigFlag)); + SuccessOrQuit(ra.AppendRecursiveDnsServerOption(&aDnsAddr, 1, 1800)); + ra.GetAsPacket(packet); + aNode.mInfraIf.SendIcmp6Nd(Ip6::Address::GetLinkLocalAllNodesMulticast(), packet.GetBytes(), packet.GetLength()); +} + +/** + * 11.3. [1.4] [CERT] Selection of upstream DNS resolver + * + * 11.3.1. Purpose + * To verify that the BR DUT: + * - Selects the upstream DNS resolver based on ND RA RDNSS Option, sent by the infrastructure network, pointing to the + * default DNS resolver. + * - Reacts to change of the upstream DNS resolver, e.g. due to a configuration change, new ISP, or other reasons. + * - Can handle link-local and global IPv6 address pointing to upstream DNS resolver. + * + * 11.3.2. Topology + * - BR_1 (DUT) - Border Router + * - Router_1 - Thread Router Reference Device, attached to BR_1 + * - ED_1 - Thread Reference Device, End Device (e.g. FED/REED) role, attached to Router_1 + * - Eth_1 - Adjacent Infrastructure Link Linux Reference Device with DNS server + * - Eth_2 - Adjacent Infrastructure Link Linux Reference Device with DNS server and RA daemon + */ +void Test_1_4_DNS_TC_3(const char *aJsonFileName) +{ + Core nexus; + Node &br1 = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + Node &ed1 = nexus.CreateNode(); + Node ð1 = nexus.CreateNode(); + Node ð2 = nexus.CreateNode(); + Ip6::Prefix gua1Prefix; + Ip6::Address eth1Addr; + Ip6::Address eth2Addr; + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote)); + + br1.SetName("BR", 1); + router1.SetName("Router", 1); + ed1.SetName("ED", 1); + eth1.SetName("Eth", 1); + eth2.SetName("Eth", 2); + + sEth1Node = ð1; + sEth2Node = ð2; + + eth1.mInfraIf.SetUdpHook(HandleUdpHook); + eth2.mInfraIf.SetUdpHook(HandleUdpHook); + + br1.AllowList(router1); + router1.AllowList(br1); + router1.AllowList(ed1); + ed1.AllowList(router1); + + SuccessOrQuit(gua1Prefix.FromString(kGua1Prefix)); + SuccessOrQuit(eth1Addr.FromString(kEth1Addr)); + SuccessOrQuit(eth2Addr.FromString(kEth2Addr)); + + /** + * Step 1 + * - Device: Eth_1, Eth_2 + * - Description (DNS-11.3): Enable + * - Pass Criteria: + * - N/A + */ + Log("Step 1: Enable Eth_1, Eth_2"); + eth1.Get().Up(); + eth2.Get().Up(); + eth1.mInfraIf.AddAddress(eth1Addr); + eth2.mInfraIf.AddAddress(eth2Addr); + + /** + * Step 2 + * - Device: Eth_2 + * - Description (DNS-11.3): Harness instructs device to: Configure router + * advertisement daemon (radvd) to multicast ND RA with: Prefix Information + * Option (PIO) with a global on-link prefix GUA_1 (SLAAC is enabled (A=1), + * on-link (L=1)), Recursive DNS Server Option (25), with Addresses of IPv6 + * Recursive DNS Servers field contains a single global IPv6 address of Eth_1. + * Configure DNS server with records: threadgroup1.org AAAA 2002:1234::E2:1, + * threadgroup2.org AAAA 2002:1234::E2:2, threadgroup3.org AAAA 2002:1234::E2:3. + * Note: prefix GUA_1 may be 2005:1234:abcd:100::/64. + * - Pass Criteria: + * - N/A + */ + Log("Step 2: Eth_2 sends RA with global IPv6 address of Eth_1"); + SendRa(eth2, gua1Prefix, eth1Addr); + + /** + * Step 3 + * - Device: Eth_1 + * - Description (DNS-11.3): Harness instructs device to: Configure DNS server + * with records: threadgroup1.org AAAA 2002:1234::E1:1, threadgroup2.org AAAA + * 2002:1234::E1:2, threadgroup3.org AAAA 2002:1234::E1:3 + * - Pass Criteria: + * - N/A + */ + Log("Step 3: Configure DNS records on Eth_1"); + // Handled by HandleUdpHook using kEth1Records. + + /** + * Step 4 + * - Device: BR_1 (DUT), Router_1, ED_1 + * - Description (DNS-11.3): Enable + * - Pass Criteria: + * - Single Thread Network forms. + */ + Log("Step 4: Enable BR_1, Router_1, ED_1"); + br1.Form(); + nexus.AdvanceTime(10000); + + router1.Join(br1, Node::kAsFtd); + ed1.Join(br1, Node::kAsFed); + + nexus.AdvanceTime(20000); + + VerifyOrQuit(br1.Get().IsAttached()); + VerifyOrQuit(router1.Get().IsRouter()); + VerifyOrQuit(ed1.Get().IsChild()); + + /** + * Step 5 + * - Device: BR_1 (DUT) + * - Description (DNS-11.3): Automatically obtains / configures an OMR prefix for + * the Thread Network, and assigns an address for its AIL interface using SLAAC. + * - Pass Criteria: + * - N/A + */ + Log("Step 5: BR_1 configures OMR prefix and AIL address"); + br1.Get().Init(1, true); + br1.Get().Init(); + SuccessOrQuit(br1.Get().SetEnabled(true)); + SuccessOrQuit(br1.Get().Start()); + br1.Get().SetUpstreamQueryEnabled(true); + + { + Dns::Client::QueryConfig dnsConfig; + dnsConfig.Clear(); + AsCoreType(&dnsConfig.mServerSockAddr.mAddress) = br1.Get().GetMeshLocalEid(); + dnsConfig.mServerSockAddr.mPort = UpstreamDns::kDnsPort; + ed1.Get().SetDefaultConfig(dnsConfig); + } + + // Resend RA to ensure BR_1 (which was just initialized) receives it. + SendRa(eth2, gua1Prefix, eth1Addr); + nexus.AdvanceTime(5000); + + /** + * Step 6 + * - Device: ED_1 + * - Description (DNS-11.3): Harness instructs device to perform DNS query + * Qtype=AAAA, name threadgroup1.org. Automatically, the DNS query gets + * routed to the DUT + * - Pass Criteria: + * - N/A + */ + Log("Step 6: ED_1 performs DNS query for threadgroup1.org"); + ed1.Get().Stop(); + SuccessOrQuit(ed1.Get().Start()); + SuccessOrQuit(ed1.Get().ResolveAddress(kThreadGroup1, HandleDnsResponse, nullptr)); + + /** + * Step 7 + * - Device: BR_1 (DUT) + * - Description (DNS-11.3): Automatically processes the DNS query by requesting + * upstream to the Eth_1 DNS server. Then, it responds back with the answer + * to ED_1. + * - Pass Criteria: + * - N/A + * + * Step 8 + * - Device: ED_1 + * - Description (DNS-11.3): Successfully receives DNS query result: + * threadgroup1.org AAAA 2002:1234::E1:1 + * - Pass Criteria: + * - ED_1 MUST receive correct DNS query answer from the DUT. + */ + Log("Step 7 & 8: Verify BR_1 requests upstream to Eth_1 and ED_1 receives correct answer"); + nexus.AdvanceTime(5000); + + /** + * Step 9 + * - Device: Eth_2 + * - Description (DNS-11.3): Harness instructs device to configure router + * advertisement daemon (radvd) to multicast ND RA with updated information, + * pointing to another DNS server: Prefix Information Option (PIO) - as + * before, Recursive DNS Server Option (25), with Addresses of IPv6 + * Recursive DNS Servers field contains a single global IPv6 address of + * Eth_2. Harness waits for a time of at least RA_PERIOD + 1 seconds, where + * RA_PERIOD is the max period between multicast ND RA transmissions by + * Eth_2, to ensure that the new ND RA is received by DUT. + * - Pass Criteria: + * - N/A + */ + Log("Step 9: Eth_2 sends RA with global IPv6 address of Eth_2"); + SendRa(eth2, gua1Prefix, eth2Addr); + nexus.AdvanceTime(10000); // Wait for RA_PERIOD + 1 seconds + + /** + * Step 10 + * - Device: ED_1 + * - Description (DNS-11.3): Harness instructs device to perform DNS query + * Qtype=AAAA, name threadgroup2.org. Automatically, the DNS query gets + * routed to the DUT. + * - Pass Criteria: + * - N/A + */ + Log("Step 10: ED_1 performs DNS query for threadgroup2.org"); + SuccessOrQuit(ed1.Get().ResolveAddress(kThreadGroup2, HandleDnsResponse, nullptr)); + + /** + * Step 11 + * - Device: BR_1 (DUT) + * - Description (DNS-11.3): Automatically processes the DNS query by requesting + * upstream to the Eth_2 DNS server. Then, it responds back with the answer + * to ED_1. + * - Pass Criteria: + * - N/A + * + * Step 12 + * - Device: ED_1 + * - Description (DNS-11.3): Successfully receives DNS query result: + * threadgroup2.org AAAA 2002:1234::E2:2 + * - Pass Criteria: + * - ED_1 MUST receive correct DNS query answer from the DUT. + */ + Log("Step 11 & 12: Verify BR_1 requests upstream to Eth_2 and ED_1 receives correct answer"); + nexus.AdvanceTime(5000); + + /** + * Step 13 + * - Device: Eth_2 + * - Description (DNS-11.3): Harness instructs device to configure router + * advertisement daemon (radvd) to multicast ND RA with updated information, + * pointing to the first DNS server using link-local address: Prefix + * Information Option (PIO) - as before, Recursive DNS Server Option (25), + * with Addresses of IPv6 Recursive DNS Servers field contains a single + * link-local IPv6 address of Eth_1. Harness waits for a time of at least + * RA_PERIOD + 1 seconds, where RA_PERIOD is the max period between + * multicast ND RA transmissions by Eth_2, to ensure that the new RA is + * received by DUT. + * - Pass Criteria: + * - N/A + */ + Log("Step 13: Eth_2 sends RA with link-local IPv6 address of Eth_1"); + { + const Ip6::Address ð1Lla = eth1.mInfraIf.GetLinkLocalAddress(); + SendRa(eth2, gua1Prefix, eth1Lla); + } + nexus.AdvanceTime(10000); // Wait for RA_PERIOD + 1 seconds + + /** + * Step 14 + * - Device: ED_1 + * - Description (DNS-11.3): Harness instructs device to perform DNS query + * Qtype=AAAA, name threadgroup3.org. Automatically, the DNS query gets + * routed to the DUT + * - Pass Criteria: + * - N/A + */ + Log("Step 14: ED_1 performs DNS query for threadgroup3.org"); + SuccessOrQuit(ed1.Get().ResolveAddress(kThreadGroup3, HandleDnsResponse, nullptr)); + + /** + * Step 15 + * - Device: BR_1 (DUT) + * - Description (DNS-11.3): Automatically processes the DNS query by requesting + * upstream to the Eth_1 DNS server. Then, it responds back with the answer + * to ED_1. + * - Pass Criteria: + * - N/A + * + * Step 16 + * - Device: ED_1 + * - Description (DNS-11.3): Successfully receives DNS query result: + * threadgroup3.org AAAA 2002:1234::E1:3 + * - Pass Criteria: + * - ED_1 MUST receive correct DNS query answer from the DUT. + */ + Log("Step 15 & 16: Verify BR_1 requests upstream to Eth_1 and ED_1 receives correct answer"); + nexus.AdvanceTime(5000); + + nexus.SaveTestInfo(aJsonFileName); +} + +} // namespace + +} // namespace Nexus +} // namespace ot + +int main(int argc, char *argv[]) +{ + ot::Nexus::Test_1_4_DNS_TC_3((argc > 2) ? argv[2] : "test_1_4_dns_tc_3.json"); + ot::Nexus::Log("All tests passed"); + return 0; +} diff --git a/tests/nexus/verify_1_4_DNS_TC_3.py b/tests/nexus/verify_1_4_DNS_TC_3.py new file mode 100644 index 000000000..04456f028 --- /dev/null +++ b/tests/nexus/verify_1_4_DNS_TC_3.py @@ -0,0 +1,280 @@ +#!/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 + +GUA1_PREFIX = "2005:1234:abcd:100::/64" +ETH1_ADDR = "2005:1234:abcd:100::e1" +ETH2_ADDR = "2005:1234:abcd:100::e2" + +THREADGROUP1 = "threadgroup1.org" +THREADGROUP2 = "threadgroup2.org" +THREADGROUP3 = "threadgroup3.org" + +ANSWER1_ETH1 = "2002:1234::e1:1" +ANSWER2_ETH2 = "2002:1234::e2:2" +ANSWER3_ETH1 = "2002:1234::e1:3" + +# ICMPv6 RA Option Types +ND_OPTION_PIO = 3 +ND_OPTION_RDNSS = 25 + + +def verify(pv): + pkts = pv.pkts + + BR_1 = pv.vars['BR_1'] + Router_1 = pv.vars['Router_1'] + ED_1 = pv.vars['ED_1'] + Eth_1 = pv.vars['Eth_1'] + Eth_2 = pv.vars['Eth_2'] + + BR_1_ETH_ADDR = pv.vars['BR_1_ETH'] + Eth_1_ETH_ADDR = pv.vars['Eth_1_ETH'] + Eth_2_ETH_ADDR = pv.vars['Eth_2_ETH'] + + # Step 1 + # - Device: Eth_1, Eth_2 + # - Description (DNS-11.3): Enable + # - Pass Criteria: + # - N/A + print("Step 1: Enable Eth_1, Eth_2") + + # Step 2 + # - Device: Eth_2 + # - Description (DNS-11.3): Harness instructs device to: Configure router + # advertisement daemon (radvd) to multicast ND RA with: Prefix Information + # Option (PIO) with a global on-link prefix GUA_1 (SLAAC is enabled (A=1), + # on-link (L=1)), Recursive DNS Server Option (25), with Addresses of IPv6 + # Recursive DNS Servers field contains a single global IPv6 address of Eth_1. + # Configure DNS server with records: threadgroup1.org AAAA 2002:1234::E2:1, + # threadgroup2.org AAAA 2002:1234::E2:2, threadgroup3.org AAAA 2002:1234::E2:3. + # Note: prefix GUA_1 may be 2005:1234:abcd:100::/64. + # - Pass Criteria: + # - N/A + print("Step 2: Eth_2 sends RA with global IPv6 address of Eth_1") + pkts.filter_eth_src(Eth_2_ETH_ADDR).\ + filter_icmpv6_nd_ra().\ + filter(lambda p: ND_OPTION_PIO in p.icmpv6.opt.type).\ + filter(lambda p: ND_OPTION_RDNSS in p.icmpv6.opt.type).\ + must_next() + + # Step 3 + # - Device: Eth_1 + # - Description (DNS-11.3): Harness instructs device to: Configure DNS server + # with records: threadgroup1.org AAAA 2002:1234::E1:1, threadgroup2.org AAAA + # 2002:1234::E1:2, threadgroup3.org AAAA 2002:1234::E1:3 + # - Pass Criteria: + # - N/A + print("Step 3: Configure DNS records on Eth_1") + + # Step 4 + # - Device: BR_1 (DUT), Router_1, ED_1 + # - Description (DNS-11.3): Enable + # - Pass Criteria: + # - Single Thread Network forms. + print("Step 4: Enable BR_1, Router_1, ED_1") + pkts.filter_wpan_src64(BR_1).\ + filter_mle_cmd(consts.MLE_ADVERTISEMENT).\ + must_next() + + pkts.filter_wpan_src64(Router_1).\ + filter_mle_cmd(consts.MLE_CHILD_ID_REQUEST).\ + must_next() + + pkts.filter_wpan_src64(ED_1).\ + filter_mle_cmd(consts.MLE_CHILD_ID_REQUEST).\ + must_next() + + # Step 5 + # - Device: BR_1 (DUT) + # - Description (DNS-11.3): Automatically obtains / configures an OMR prefix for + # the Thread Network, and assigns an address for its AIL interface using SLAAC. + # - Pass Criteria: + # - N/A + print("Step 5: BR_1 configures OMR prefix and AIL address") + + # Step 6 + # - Device: ED_1 + # - Description (DNS-11.3): Harness instructs device to perform DNS query + # Qtype=AAAA, name threadgroup1.org. Automatically, the DNS query gets + # routed to the DUT + # - Pass Criteria: + # - N/A + print("Step 6: ED_1 performs DNS query for threadgroup1.org") + pkts.filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\ + must_next() + + # Step 7 + # - Device: BR_1 (DUT) + # - Description (DNS-11.3): Automatically processes the DNS query by requesting + # upstream to the Eth_1 DNS server. Then, it responds back with the answer + # to ED_1. + # - Pass Criteria: + # - N/A + print("Step 7: BR_1 requests upstream to Eth_1") + pkts.filter_eth_src(BR_1_ETH_ADDR).\ + filter(lambda p: p.eth.dst == Eth_1_ETH_ADDR).\ + filter_ipv6_dst(ETH1_ADDR).\ + filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\ + must_next() + + # Step 8 + # - Device: ED_1 + # - Description (DNS-11.3): Successfully receives DNS query result: + # threadgroup1.org AAAA 2002:1234::E1:1 + # - Pass Criteria: + # - ED_1 MUST receive correct DNS query answer from the DUT. + print("Step 8: ED_1 receives correct answer") + pkts.filter_eth_src(Eth_1_ETH_ADDR).\ + filter(lambda p: p.eth.dst == BR_1_ETH_ADDR).\ + must_next() + + # Step 9 + # - Device: Eth_2 + # - Description (DNS-11.3): Harness instructs device to configure router + # advertisement daemon (radvd) to multicast ND RA with updated information, + # pointing to another DNS server: Prefix Information Option (PIO) - as + # before, Recursive DNS Server Option (25), with Addresses of IPv6 + # Recursive DNS Servers field contains a single global IPv6 address of + # Eth_2. Harness waits for a time of at least RA_PERIOD + 1 seconds, where + # RA_PERIOD is the max period between multicast ND RA transmissions by + # Eth_2, to ensure that the new ND RA is received by DUT. + # - Pass Criteria: + # - N/A + print("Step 9: Eth_2 sends RA with global IPv6 address of Eth_2") + pkts.filter_eth_src(Eth_2_ETH_ADDR).\ + filter_icmpv6_nd_ra().\ + filter(lambda p: ND_OPTION_PIO in p.icmpv6.opt.type).\ + filter(lambda p: ND_OPTION_RDNSS in p.icmpv6.opt.type).\ + must_next() + + # Step 10 + # - Device: ED_1 + # - Description (DNS-11.3): Harness instructs device to perform DNS query + # Qtype=AAAA, name threadgroup2.org. Automatically, the DNS query gets + # routed to the DUT. + # - Pass Criteria: + # - N/A + print("Step 10: ED_1 performs DNS query for threadgroup2.org") + pkts.filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\ + must_next() + + # Step 11 + # - Device: BR_1 (DUT) + # - Description (DNS-11.3): Automatically processes the DNS query by requesting + # upstream to the Eth_2 DNS server. Then, it responds back with the answer + # to ED_1. + # - Pass Criteria: + # - N/A + print("Step 11: BR_1 requests upstream to Eth_2") + pkts.filter_eth_src(BR_1_ETH_ADDR).\ + filter(lambda p: p.eth.dst == Eth_2_ETH_ADDR).\ + filter_ipv6_dst(ETH2_ADDR).\ + filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\ + must_next() + + # Step 12 + # - Device: ED_1 + # - Description (DNS-11.3): Successfully receives DNS query result: + # threadgroup2.org AAAA 2002:1234::E2:2 + # - Pass Criteria: + # - ED_1 MUST receive correct DNS query answer from the DUT. + print("Step 12: ED_1 receives correct answer") + pkts.filter_eth_src(Eth_2_ETH_ADDR).\ + filter(lambda p: p.eth.dst == BR_1_ETH_ADDR).\ + must_next() + + # Step 13 + # - Device: Eth_2 + # - Description (DNS-11.3): Harness instructs device to configure router + # advertisement daemon (radvd) to multicast ND RA with updated information, + # pointing to the first DNS server using link-local address: Prefix + # Information Option (PIO) - as before, Recursive DNS Server Option (25), + # with Addresses of IPv6 Recursive DNS Servers field contains a single + # link-local IPv6 address of Eth_1. Harness waits for a time of at least + # RA_PERIOD + 1 seconds, where RA_PERIOD is the max period between + # multicast ND RA transmissions by Eth_2, to ensure that the new RA is + # received by DUT. + # - Pass Criteria: + # - N/A + print("Step 13: Eth_2 sends RA with link-local IPv6 address of Eth_1") + pkts.filter_eth_src(Eth_2_ETH_ADDR).\ + filter_icmpv6_nd_ra().\ + filter(lambda p: ND_OPTION_PIO in p.icmpv6.opt.type).\ + filter(lambda p: ND_OPTION_RDNSS in p.icmpv6.opt.type).\ + must_next() + + # Step 14 + # - Device: ED_1 + # - Description (DNS-11.3): Harness instructs device to perform DNS query + # Qtype=AAAA, name threadgroup3.org. Automatically, the DNS query gets + # routed to the DUT + # - Pass Criteria: + # - N/A + print("Step 14: ED_1 performs DNS query for threadgroup3.org") + pkts.filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\ + must_next() + + # Step 15 + # - Device: BR_1 (DUT) + # - Description (DNS-11.3): Automatically processes the DNS query by requesting + # upstream to the Eth_1 DNS server. Then, it responds back with the answer + # to ED_1. + # - Pass Criteria: + # - N/A + print("Step 15: BR_1 requests upstream to Eth_1") + pkts.filter_eth_src(BR_1_ETH_ADDR).\ + filter(lambda p: p.eth.dst == Eth_1_ETH_ADDR).\ + filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\ + must_next() + + # Step 16 + # - Device: ED_1 + # - Description (DNS-11.3): Successfully receives DNS query result: + # threadgroup3.org AAAA 2002:1234::E1:3 + # - Pass Criteria: + # - ED_1 MUST receive correct DNS query answer from the DUT. + print("Step 16: ED_1 receives correct answer") + pkts.filter_eth_src(Eth_1_ETH_ADDR).\ + filter(lambda p: p.eth.dst == BR_1_ETH_ADDR).\ + must_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify)