[nexus] implement test 1_4_DNS_TC_3 for upstream DNS resolver selection (#12835)

This commit implements the Nexus test specification 1_4_DNS_TC_3 for
upstream DNS resolver selection in OpenThread.

Nexus Platform Enhancements:
- Added OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE and
  OPENTHREAD_CONFIG_PLATFORM_DNS_ENABLE to nexus config.
- Implemented platform DNS APIs in nexus_dns.cpp, supporting
  upstream server selection based on prefix lifetimes and reachability.
- Added UdpHook to Core to allow tests to intercept and simulate
  responses for backbone UDP traffic on port 53.
- Updated InfraIf::Receive to call Core::HandleUdp for generic UDP
  interception.
- Added raw buffer delivery overloads for InfraIf::SendUdp.

Test Implementation:
- Created test_1_4_DNS_TC_3.cpp which performs network formation,
  RA signaling (PIO/RIO/RDNSS), and DNS resolution triggers.
- Created verify_1_4_DNS_TC_3.py to validate network behavior,
  RA contents, and correct upstream query routing using pktverify.
- Integrated the new test into CMakeLists.txt and the default
  run_nexus_tests.sh suite.
This commit is contained in:
Jonathan Hui
2026-04-08 17:01:03 -05:00
committed by GitHub
parent 2b3b56def7
commit 3b84b4c5cb
13 changed files with 1170 additions and 53 deletions
+2
View File
@@ -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
+3 -2
View File
@@ -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_
+23 -19
View File
@@ -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<Ip6::Netif>().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<ThreadNetif>().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)
{
+213
View File
@@ -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 <openthread/platform/dns.h>
#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::RxRaTracker>();
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::RxRaTracker>();
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<Ip6::Udp>().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<PendingQuery> 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<MessagePool>().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
+87
View File
@@ -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 <openthread/platform/dns.h>
#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<PendingQuery>, public LinkedListEntry<PendingQuery>
{
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<PendingQuery> mPendingQueries;
};
} // namespace Nexus
} // namespace ot
#endif // OT_NEXUS_PLATFORM_NEXUS_DNS_HPP_
+59 -17
View File
@@ -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<MessagePool>().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<Ip6::Ip6>().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<const otIp6Address *>(&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).
+6 -5
View File
@@ -58,6 +58,8 @@ public:
const Ip6::Address &GetLinkLocalAddress(void) const { return mAddresses[0]; }
const Heap::Array<Ip6::Address> &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<Ip6::Address> mAddresses;
Callback<EchoReplyHandler> mEchoReplyCallback;
UdpHook mUdpHook;
Ip6::Prefix mPioPrefix;
Ip6::Prefix mRioPrefix;
+1
View File
@@ -40,6 +40,7 @@ void Node::Reset(void)
mAlarmMilli.Reset();
mAlarmMicro.Reset();
mMdns.Reset();
mUpstreamDns.Reset();
mInfraIf.mPendingTxQueue.DequeueAndFreeAll();
mPendingTasklet = false;
+13 -9
View File
@@ -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
+1
View File
@@ -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"
)
+7 -1
View File
@@ -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<Ip6::Ip6>().NewMessage();
VerifyOrQuit(message != nullptr);
SuccessOrQuit(message->SetLength(kUdpPayloadSize));
host.mInfraIf.SendUdp(*hostUla, ma1, kCoapPort, kCoapPort, *message);
}
nexus.AdvanceTime(0);
/**
+475
View File
@@ -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<MessagePool>().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 &eth1 = nexus.CreateNode();
Node &eth2 = 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 = &eth1;
sEth2Node = &eth2;
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<ThreadNetif>().Up();
eth2.Get<ThreadNetif>().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<Mle::Mle>().IsAttached());
VerifyOrQuit(router1.Get<Mle::Mle>().IsRouter());
VerifyOrQuit(ed1.Get<Mle::Mle>().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<BorderRouter::InfraIf>().Init(1, true);
br1.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().SetEnabled(true));
SuccessOrQuit(br1.Get<Dns::ServiceDiscovery::Server>().Start());
br1.Get<Dns::ServiceDiscovery::Server>().SetUpstreamQueryEnabled(true);
{
Dns::Client::QueryConfig dnsConfig;
dnsConfig.Clear();
AsCoreType(&dnsConfig.mServerSockAddr.mAddress) = br1.Get<Mle::Mle>().GetMeshLocalEid();
dnsConfig.mServerSockAddr.mPort = UpstreamDns::kDnsPort;
ed1.Get<Dns::Client>().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<Dns::Client>().Stop();
SuccessOrQuit(ed1.Get<Dns::Client>().Start());
SuccessOrQuit(ed1.Get<Dns::Client>().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<Dns::Client>().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 &eth1Lla = 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<Dns::Client>().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;
}
+280
View File
@@ -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)