From e2e5cba98cbfdf3b1e6e3fccdcdaf4450fcfc4db Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Mon, 27 Apr 2026 18:57:22 -0700 Subject: [PATCH] [nexus] migrate test_dnssd.py to nexus (#12975) This commit migrates the DNS-SD test from the thread-cert Python framework to the Nexus C++ framework. The new Nexus test 'test_dnssd.cpp' replicates the original test scenario and functionality: - Formation of a Thread network with multiple SRP clients and a server. - Service registration with subtypes via SRP. - DNS browsing for full service types and specific subtypes. - DNS address (AAAA) and service (SRV/TXT/AAAA) resolution. - Specific DNS record queries for SRV and KEY record types. - Verification of DNS behavior for non-existent records. The original Python script 'tests/scripts/thread-cert/test_dnssd.py' is removed as its functionality is now fully covered by Nexus. Nexus tests provide faster and more scalable network simulations within a single process, improving CI efficiency and reliability. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/test_dnssd.cpp | 524 ++++++++++++++++++++++++ tests/scripts/thread-cert/test_dnssd.py | 297 -------------- 3 files changed, 525 insertions(+), 297 deletions(-) create mode 100644 tests/nexus/test_dnssd.cpp delete mode 100755 tests/scripts/thread-cert/test_dnssd.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 4d88effc9..f1589f5a5 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -393,6 +393,7 @@ ot_nexus_test(border_agent_tracker "core;nexus") ot_nexus_test(child_supervision "core;nexus") ot_nexus_test(dataset_updater "core;nexus") ot_nexus_test(discover_scan "core;nexus") +ot_nexus_test(dnssd "core;nexus") ot_nexus_test(dns_client_config_auto_start "core;nexus") ot_nexus_test(dtls "core;nexus") ot_nexus_test(form_join "core;nexus") diff --git a/tests/nexus/test_dnssd.cpp b/tests/nexus/test_dnssd.cpp new file mode 100644 index 000000000..98ebbbbb5 --- /dev/null +++ b/tests/nexus/test_dnssd.cpp @@ -0,0 +1,524 @@ +/* + * Copyright (c) 2026, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include "common/as_core_type.hpp" +#include "common/string.hpp" +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +namespace { + +static constexpr uint32_t kFormNetworkTime = 13 * 1000; +static constexpr uint32_t kJoinNetworkTime = 10 * 1000; +static constexpr uint32_t kStabilizationTime = 200 * 1000; +static constexpr uint32_t kDnsQueryTime = 5 * 1000; + +static const char kService[] = "_ipps._tcp"; +static const char kFullServiceType[] = "_ipps._tcp.default.service.arpa."; +static const char kSubtype1ServiceType[] = "_s1._sub._ipps._tcp.default.service.arpa."; +static const char kSubtype2ServiceType[] = "_s2._sub._ipps._tcp.default.service.arpa."; + +struct BrowseContext +{ + Error mError; + uint8_t mCount; + Dns::Name::LabelBuffer mLabels[10]; + + void Reset(void) + { + mError = kErrorNone; + mCount = 0; + } + + bool ContainsInstance(const char *aLabel) const + { + bool found = false; + + for (uint8_t i = 0; i < mCount; i++) + { + if (StringMatch(mLabels[i], aLabel, kStringCaseInsensitiveMatch)) + { + found = true; + break; + } + } + + return found; + } +}; + +struct ResolveContext +{ + Error mError; + Dns::Name::LabelBuffer mInstanceLabel; + Dns::Name::Buffer mFullServiceName; + Dns::Name::Buffer mHostName; + uint8_t mTxtData[128]; + Dns::Client::ServiceInfo mServiceInfo; + + void Reset(void) + { + mError = kErrorNone; + mInstanceLabel[0] = '\0'; + mFullServiceName[0] = '\0'; + mHostName[0] = '\0'; + ClearAllBytes(mServiceInfo); + mServiceInfo.mHostNameBuffer = mHostName; + mServiceInfo.mHostNameBufferSize = sizeof(mHostName); + mServiceInfo.mTxtData = mTxtData; + mServiceInfo.mTxtDataSize = sizeof(mTxtData); + } +}; + +struct AddressContext +{ + Error mError; + Ip6::Address mAddresses[10]; + uint8_t mCount; + + void Reset(void) + { + mError = kErrorNone; + mCount = 0; + } +}; + +struct RecordContext +{ + Error mError; + uint8_t mCount; + uint16_t mType; + uint16_t mLength; + uint32_t mTtl; + Dns::Name::Buffer mName; + + void Reset(void) + { + mError = kErrorNone; + mCount = 0; + mType = 0; + mLength = 0; + mTtl = 0; + mName[0] = '\0'; + } +}; + +void HandleBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse, void *aContext) +{ + BrowseContext *context = static_cast(aContext); + const Dns::Client::BrowseResponse &response = AsCoreType(aResponse); + + context->mError = static_cast(aError); + SuccessOrExit(context->mError); + + while (response.GetServiceInstance(context->mCount, context->mLabels[context->mCount], + sizeof(context->mLabels[context->mCount])) == kErrorNone) + { + context->mCount++; + } + +exit: + return; +} + +void HandleResolveResponse(otError aError, const otDnsServiceResponse *aResponse, void *aContext) +{ + ResolveContext *context = static_cast(aContext); + const Dns::Client::ServiceResponse &response = AsCoreType(aResponse); + + context->mError = static_cast(aError); + SuccessOrExit(context->mError); + + SuccessOrExit(response.GetServiceName(context->mInstanceLabel, sizeof(context->mInstanceLabel), + context->mFullServiceName, sizeof(context->mFullServiceName))); + SuccessOrExit(response.GetServiceInfo(context->mServiceInfo)); + +exit: + return; +} + +void HandleAddressResolveResponse(otError aError, const otDnsAddressResponse *aResponse, void *aContext) +{ + AddressContext *context = static_cast(aContext); + const Dns::Client::AddressResponse &response = AsCoreType(aResponse); + uint32_t ttl; + + context->mError = static_cast(aError); + SuccessOrExit(context->mError); + + while (response.GetAddress(context->mCount, context->mAddresses[context->mCount], ttl) == kErrorNone) + { + context->mCount++; + } + +exit: + return; +} + +void HandleRecordQueryResponse(otError aError, const otDnsRecordResponse *aResponse, void *aContext) +{ + RecordContext *context = static_cast(aContext); + const Dns::Client::RecordResponse &response = AsCoreType(aResponse); + Dns::Client::RecordInfo recordInfo; + + context->mError = static_cast(aError); + SuccessOrExit(context->mError); + + recordInfo.mNameBuffer = context->mName; + recordInfo.mNameBufferSize = sizeof(context->mName); + + for (uint16_t index = 0; response.GetRecordInfo(index, recordInfo) == kErrorNone; index++) + { + if (recordInfo.mRecordType == context->mType) + { + context->mCount++; + context->mLength = recordInfo.mRecordLength; + context->mTtl = recordInfo.mTtl; + break; + } + } + +exit: + return; +} + +} // namespace + +void TestDnssd(const char *aJsonFileName) +{ + Core nexus; + Node &server = nexus.CreateNode(); + Node &client1 = nexus.CreateNode(); + Node &client2 = nexus.CreateNode(); + Node &client3 = nexus.CreateNode(); + + static Srp::Client::Service srv1, srv2, srv3; + static const char *const kSubtypes1[] = {"_s1", "_s2", nullptr}; + static const char *const kSubtypes3[] = {"_s1", nullptr}; + + static Ip6::Address addrs1[2], addrs2[2], addrs3[2]; + + server.SetName("Server"); + client1.SetName("Client1"); + client2.SetName("Client2"); + client3.SetName("Client3"); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote)); + + //----------------------------------------------------------------------------------------- + // Step 1: Start topology + Log("Step 1: Start topology"); + + server.Form(); + nexus.AdvanceTime(kFormNetworkTime); + + client1.Join(server); + client2.Join(server); + client3.Join(server); + nexus.AdvanceTime(kJoinNetworkTime); + + server.Get().SetEnabled(true); + SuccessOrQuit(server.Get().Start()); + + nexus.AdvanceTime(kStabilizationTime); + + //----------------------------------------------------------------------------------------- + // Step 2: Register services on clients + Log("Step 2: Register services on clients"); + + client1.Get().EnableAutoStartMode(nullptr, nullptr); + client2.Get().EnableAutoStartMode(nullptr, nullptr); + client3.Get().EnableAutoStartMode(nullptr, nullptr); + + addrs1[0] = client1.Get().GetMeshLocalEid(); + addrs1[1] = client1.Get().GetMeshLocalRloc(); + SuccessOrQuit(client1.Get().SetHostName("host1")); + SuccessOrQuit(client1.Get().SetHostAddresses(addrs1, 2)); + ClearAllBytes(srv1); + srv1.mName = kService; + srv1.mInstanceName = "ins1"; + srv1.mPort = 11111; + srv1.mPriority = 1; + srv1.mWeight = 1; + srv1.mSubTypeLabels = kSubtypes1; + SuccessOrQuit(srv1.Init()); + SuccessOrQuit(client1.Get().AddService(srv1)); + + addrs2[0] = client2.Get().GetMeshLocalEid(); + addrs2[1] = client2.Get().GetMeshLocalRloc(); + SuccessOrQuit(client2.Get().SetHostName("host2")); + SuccessOrQuit(client2.Get().SetHostAddresses(addrs2, 2)); + ClearAllBytes(srv2); + srv2.mName = kService; + srv2.mInstanceName = "ins2"; + srv2.mPort = 22222; + srv2.mPriority = 2; + srv2.mWeight = 2; + SuccessOrQuit(srv2.Init()); + SuccessOrQuit(client2.Get().AddService(srv2)); + + addrs3[0] = client3.Get().GetMeshLocalEid(); + addrs3[1] = client3.Get().GetMeshLocalRloc(); + SuccessOrQuit(client3.Get().SetHostName("host3")); + SuccessOrQuit(client3.Get().SetHostAddresses(addrs3, 2)); + ClearAllBytes(srv3); + srv3.mName = kService; + srv3.mInstanceName = "ins3"; + srv3.mPort = 33333; + srv3.mPriority = 3; + srv3.mWeight = 3; + srv3.mSubTypeLabels = kSubtypes3; + SuccessOrQuit(srv3.Init()); + SuccessOrQuit(client3.Get().AddService(srv3)); + + nexus.AdvanceTime(kStabilizationTime); + + //----------------------------------------------------------------------------------------- + // Step 3: Resolve address (AAAA records) + Log("Step 3: Resolve address (AAAA records)"); + + AddressContext addressContext; + addressContext.Reset(); + + Dns::Client::QueryConfig queryConfig; + queryConfig.Clear(); + AsCoreType(&queryConfig.mServerSockAddr).SetAddress(server.Get().GetMeshLocalEid()); + queryConfig.mServerSockAddr.mPort = 53; + client1.Get().SetDefaultConfig(queryConfig); + + SuccessOrQuit(client1.Get().ResolveAddress("host1.default.service.arpa.", HandleAddressResolveResponse, + &addressContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(addressContext.mError); + VerifyOrQuit(addressContext.mCount == 2); + VerifyOrQuit(addressContext.mAddresses[0] == client1.Get().GetMeshLocalEid() || + addressContext.mAddresses[0] == client1.Get().GetMeshLocalRloc()); + VerifyOrQuit(addressContext.mAddresses[1] == client1.Get().GetMeshLocalEid() || + addressContext.mAddresses[1] == client1.Get().GetMeshLocalRloc()); + + addressContext.Reset(); + SuccessOrQuit(client1.Get().ResolveAddress("host2.default.service.arpa.", HandleAddressResolveResponse, + &addressContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(addressContext.mError); + VerifyOrQuit(addressContext.mCount == 2); + VerifyOrQuit(addressContext.mAddresses[0] == client2.Get().GetMeshLocalEid() || + addressContext.mAddresses[0] == client2.Get().GetMeshLocalRloc()); + VerifyOrQuit(addressContext.mAddresses[1] == client2.Get().GetMeshLocalEid() || + addressContext.mAddresses[1] == client2.Get().GetMeshLocalRloc()); + + //----------------------------------------------------------------------------------------- + // Step 4: Browsing for services + Log("Step 4: Browsing for services"); + + BrowseContext browseContext; + browseContext.Reset(); + SuccessOrQuit(client1.Get().Browse(kFullServiceType, HandleBrowseResponse, &browseContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(browseContext.mError); + VerifyOrQuit(browseContext.mCount == 3); + VerifyOrQuit(browseContext.ContainsInstance("ins1")); + VerifyOrQuit(browseContext.ContainsInstance("ins2")); + VerifyOrQuit(browseContext.ContainsInstance("ins3")); + + // Browse for subtype _s1 + browseContext.Reset(); + SuccessOrQuit(client1.Get().Browse(kSubtype1ServiceType, HandleBrowseResponse, &browseContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(browseContext.mError); + VerifyOrQuit(browseContext.mCount == 2); + VerifyOrQuit(browseContext.ContainsInstance("ins1")); + VerifyOrQuit(browseContext.ContainsInstance("ins3")); + + // Browse for subtype _s2 + browseContext.Reset(); + SuccessOrQuit(client1.Get().Browse(kSubtype2ServiceType, HandleBrowseResponse, &browseContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(browseContext.mError); + VerifyOrQuit(browseContext.mCount == 1); + VerifyOrQuit(browseContext.ContainsInstance("ins1")); + + //----------------------------------------------------------------------------------------- + // Step 5: Resolve service + Log("Step 5: Resolve service"); + + ResolveContext resolveContext; + + resolveContext.Reset(); + SuccessOrQuit( + client1.Get().ResolveService("ins1", kFullServiceType, HandleResolveResponse, &resolveContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(resolveContext.mError); + VerifyOrQuit(StringMatch(resolveContext.mInstanceLabel, "ins1", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringMatch(resolveContext.mFullServiceName, kFullServiceType, kStringCaseInsensitiveMatch)); + VerifyOrQuit(resolveContext.mServiceInfo.mPort == 11111); + VerifyOrQuit(resolveContext.mServiceInfo.mPriority == 1); + VerifyOrQuit(resolveContext.mServiceInfo.mWeight == 1); + VerifyOrQuit(StringMatch(resolveContext.mHostName, "host1.default.service.arpa.", kStringCaseInsensitiveMatch)); + VerifyOrQuit(AsCoreType(&resolveContext.mServiceInfo.mHostAddress) == client1.Get().GetMeshLocalEid() || + AsCoreType(&resolveContext.mServiceInfo.mHostAddress) == client1.Get().GetMeshLocalRloc()); + VerifyOrQuit(resolveContext.mServiceInfo.mTtl > 0); + VerifyOrQuit(resolveContext.mServiceInfo.mHostAddressTtl > 0); + + resolveContext.Reset(); + SuccessOrQuit( + client1.Get().ResolveService("ins2", kFullServiceType, HandleResolveResponse, &resolveContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(resolveContext.mError); + VerifyOrQuit(StringMatch(resolveContext.mInstanceLabel, "ins2", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringMatch(resolveContext.mFullServiceName, kFullServiceType, kStringCaseInsensitiveMatch)); + VerifyOrQuit(resolveContext.mServiceInfo.mPort == 22222); + VerifyOrQuit(resolveContext.mServiceInfo.mPriority == 2); + VerifyOrQuit(resolveContext.mServiceInfo.mWeight == 2); + VerifyOrQuit(StringMatch(resolveContext.mHostName, "host2.default.service.arpa.", kStringCaseInsensitiveMatch)); + VerifyOrQuit(AsCoreType(&resolveContext.mServiceInfo.mHostAddress) == client2.Get().GetMeshLocalEid() || + AsCoreType(&resolveContext.mServiceInfo.mHostAddress) == client2.Get().GetMeshLocalRloc()); + + resolveContext.Reset(); + SuccessOrQuit( + client1.Get().ResolveService("ins3", kFullServiceType, HandleResolveResponse, &resolveContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(resolveContext.mError); + VerifyOrQuit(StringMatch(resolveContext.mInstanceLabel, "ins3", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringMatch(resolveContext.mFullServiceName, kFullServiceType, kStringCaseInsensitiveMatch)); + VerifyOrQuit(resolveContext.mServiceInfo.mPort == 33333); + VerifyOrQuit(resolveContext.mServiceInfo.mPriority == 3); + VerifyOrQuit(resolveContext.mServiceInfo.mWeight == 3); + VerifyOrQuit(StringMatch(resolveContext.mHostName, "host3.default.service.arpa.", kStringCaseInsensitiveMatch)); + VerifyOrQuit(AsCoreType(&resolveContext.mServiceInfo.mHostAddress) == client3.Get().GetMeshLocalEid() || + AsCoreType(&resolveContext.mServiceInfo.mHostAddress) == client3.Get().GetMeshLocalRloc()); + + //----------------------------------------------------------------------------------------- + // Step 6: Add another service with TXT entries + Log("Step 6: Add another service with TXT entries"); + + Srp::Client::Service srv4; + { + static const Dns::TxtEntry kTxtEntries[] = {{"KEY", reinterpret_cast("ABC"), 3}}; + + ClearAllBytes(srv4); + srv4.mName = kService; + srv4.mInstanceName = "ins4"; + srv4.mPort = 44444; + srv4.mPriority = 4; + srv4.mWeight = 4; + srv4.mTxtEntries = kTxtEntries; + srv4.mNumTxtEntries = 1; + srv4.mSubTypeLabels = kSubtypes3; + + SuccessOrQuit(srv4.Init()); + SuccessOrQuit(client3.Get().AddService(srv4)); + } + nexus.AdvanceTime(kStabilizationTime); + + browseContext.Reset(); + SuccessOrQuit(client1.Get().Browse(kFullServiceType, HandleBrowseResponse, &browseContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(browseContext.mError); + VerifyOrQuit(browseContext.mCount == 4); + VerifyOrQuit(browseContext.ContainsInstance("ins1")); + VerifyOrQuit(browseContext.ContainsInstance("ins2")); + VerifyOrQuit(browseContext.ContainsInstance("ins3")); + VerifyOrQuit(browseContext.ContainsInstance("ins4")); + + resolveContext.Reset(); + SuccessOrQuit( + client1.Get().ResolveService("ins4", kFullServiceType, HandleResolveResponse, &resolveContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(resolveContext.mError); + VerifyOrQuit(StringMatch(resolveContext.mInstanceLabel, "ins4", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringMatch(resolveContext.mFullServiceName, kFullServiceType, kStringCaseInsensitiveMatch)); + VerifyOrQuit(resolveContext.mServiceInfo.mPort == 44444); + VerifyOrQuit(resolveContext.mServiceInfo.mPriority == 4); + VerifyOrQuit(resolveContext.mServiceInfo.mWeight == 4); + VerifyOrQuit(resolveContext.mServiceInfo.mTxtDataSize == 8); + VerifyOrQuit(memcmp(resolveContext.mServiceInfo.mTxtData, "\x07KEY=ABC", 8) == 0); + + //----------------------------------------------------------------------------------------- + // Step 7: Query for KEY record + Log("Step 7: Query for KEY record"); + + RecordContext recordContext; + recordContext.Reset(); + recordContext.mType = 25; // KEY + SuccessOrQuit(client1.Get().QueryRecord(25, nullptr, "ins1._ipps._tcp.default.service.arpa.", + HandleRecordQueryResponse, &recordContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(recordContext.mError); + VerifyOrQuit(recordContext.mCount == 1); + VerifyOrQuit(recordContext.mLength == 78); + VerifyOrQuit(recordContext.mTtl > 0); + VerifyOrQuit( + StringMatch(recordContext.mName, "ins1._ipps._tcp.default.service.arpa.", kStringCaseInsensitiveMatch)); + + //----------------------------------------------------------------------------------------- + // Step 8: Query for SRV record + Log("Step 8: Query for SRV record"); + + recordContext.Reset(); + recordContext.mType = 33; // SRV + SuccessOrQuit(client1.Get().QueryRecord(33, nullptr, "ins1._ipps._tcp.default.service.arpa.", + HandleRecordQueryResponse, &recordContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(recordContext.mError); + VerifyOrQuit(recordContext.mCount == 1); + VerifyOrQuit(recordContext.mLength > 0); + VerifyOrQuit(recordContext.mTtl > 0); + VerifyOrQuit( + StringMatch(recordContext.mName, "ins1._ipps._tcp.default.service.arpa.", kStringCaseInsensitiveMatch)); + + //----------------------------------------------------------------------------------------- + // Step 9: Query for non-existing A record + Log("Step 9: Query for non-existing A record"); + + recordContext.Reset(); + recordContext.mType = 1; // A + SuccessOrQuit(client1.Get().QueryRecord(1, nullptr, "ins1._ipps._tcp.default.service.arpa.", + HandleRecordQueryResponse, &recordContext)); + nexus.AdvanceTime(kDnsQueryTime); + SuccessOrQuit(recordContext.mError); + VerifyOrQuit(recordContext.mCount == 0); + + nexus.SaveTestInfo(aJsonFileName); +} + +} // namespace Nexus +} // namespace ot + +int main(int argc, char *argv[]) +{ + ot::Nexus::TestDnssd((argc > 2) ? argv[2] : "test_dnssd.json"); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/thread-cert/test_dnssd.py b/tests/scripts/thread-cert/test_dnssd.py deleted file mode 100755 index 5642a0e1e..000000000 --- a/tests/scripts/thread-cert/test_dnssd.py +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2021, The OpenThread Authors. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -import ipaddress -import typing -import unittest - -import config -import thread_cert - -# Test description: -# -# This test verifies DNS-SD server and DNS client behavior -# (browsing for services and/or subtype services, resolving an -# address, or resolving a service). It also indirectly covers the SRP -# client and server behavior and the interactions of DNS-SD server -# with SRP server). -# -# Topology: -# Four nodes, leader acting as SRP and DNS-SD servers, with 3 router -# nodes acting as SRP and DNS clients. -# - -SERVER = 1 -CLIENT1 = 2 -CLIENT2 = 3 -CLIENT3 = 4 - -DOMAIN = 'default.service.arpa.' -SERVICE = '_ipps._tcp' - - -class TestDnssd(thread_cert.TestCase): - SUPPORT_NCP = False - USE_MESSAGE_FACTORY = False - - TOPOLOGY = { - SERVER: { - 'mode': 'rdn', - }, - CLIENT1: { - 'mode': 'rdn', - }, - CLIENT2: { - 'mode': 'rdn', - }, - CLIENT3: { - 'mode': 'rdn', - } - } - - def test(self): - server = self.nodes[SERVER] - client1 = self.nodes[CLIENT1] - client2 = self.nodes[CLIENT2] - client3 = self.nodes[CLIENT3] - - #--------------------------------------------------------------- - # Start the server & client devices. - - server.start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(server.get_state(), 'leader') - server.srp_server_set_enabled(True) - - client1.start() - client2.start() - client3.start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - self.assertEqual(client1.get_state(), 'router') - self.assertEqual(client2.get_state(), 'router') - self.assertEqual(client3.get_state(), 'router') - - #--------------------------------------------------------------- - # Register services on clients - - client1_addrs = [client1.get_mleid(), client1.get_rloc()] - client2_addrs = [client2.get_mleid(), client2.get_rloc()] - client3_addrs = [client3.get_mleid(), client2.get_rloc()] - - self._config_srp_client_services(client1, server, 'ins1', 'host1', 11111, 1, 1, client1_addrs, ",_s1,_s2") - self._config_srp_client_services(client2, server, 'ins2', 'HOST2', 22222, 2, 2, client2_addrs) - self._config_srp_client_services(client3, server, 'ins3', 'host3', 33333, 3, 3, client3_addrs, ",_S1") - - #--------------------------------------------------------------- - # Resolve address (AAAA records) - - answers = client1.dns_resolve(f"host1.{DOMAIN}".upper(), server.get_mleid(), 53) - self.assertEqual(set(ipaddress.IPv6Address(ip) for ip, _ in answers), - set(map(ipaddress.IPv6Address, client1_addrs))) - - answers = client1.dns_resolve(f"host2.{DOMAIN}", server.get_mleid(), 53) - self.assertEqual(set(ipaddress.IPv6Address(ip) for ip, _ in answers), - set(map(ipaddress.IPv6Address, client2_addrs))) - - #--------------------------------------------------------------- - # Browsing for services - - instance1_verify_info = { - 'port': 11111, - 'priority': 1, - 'weight': 1, - 'host': 'host1.default.service.arpa.', - 'address': client1_addrs, - 'txt_data': '', - 'srv_ttl': lambda x: x > 0, - 'txt_ttl': lambda x: x > 0, - 'aaaa_ttl': lambda x: x > 0, - } - - instance2_verify_info = { - 'port': 22222, - 'priority': 2, - 'weight': 2, - 'host': 'host2.default.service.arpa.', - 'address': client2_addrs, - 'txt_data': '', - 'srv_ttl': lambda x: x > 0, - 'txt_ttl': lambda x: x > 0, - 'aaaa_ttl': lambda x: x > 0, - } - - instance3_verify_info = { - 'port': 33333, - 'priority': 3, - 'weight': 3, - 'host': 'host3.default.service.arpa.', - 'address': client3_addrs, - 'txt_data': '', - 'srv_ttl': lambda x: x > 0, - 'txt_ttl': lambda x: x > 0, - 'aaaa_ttl': lambda x: x > 0, - } - - instance4_verify_info = { - 'port': 44444, - 'priority': 4, - 'weight': 4, - 'host': 'host3.default.service.arpa.', - 'address': client3_addrs, - 'txt_data': 'KEY=414243', # KEY=ABC - 'srv_ttl': lambda x: x > 0, - 'txt_ttl': lambda x: x > 0, - 'aaaa_ttl': lambda x: x > 0, - } - - # Browse for main service - service_instances = client1.dns_browse(f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) - self.assertEqual({'ins1', 'ins2', 'ins3'}, set(service_instances.keys())) - - # Browse for service sub-type _s1. - service_instances = client1.dns_browse(f'_s1._sub.{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) - self.assertEqual({'ins1', 'ins3'}, set(service_instances.keys())) - - # Browse for service sub-type _s2. - # Since there is only one matching instance, validate that - # server included the service info in additional section. - service_instances = client1.dns_browse(f'_s2._sub.{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) - self.assertEqual({'ins1'}, set(service_instances.keys())) - self._assert_service_instance_equal(service_instances['ins1'], instance1_verify_info) - - #--------------------------------------------------------------- - # Resolve service - - service_instance = client1.dns_resolve_service('ins1', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) - self._assert_service_instance_equal(service_instance, instance1_verify_info) - - service_instance = client1.dns_resolve_service('ins2', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) - self._assert_service_instance_equal(service_instance, instance2_verify_info) - - service_instance = client1.dns_resolve_service('ins3', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) - self._assert_service_instance_equal(service_instance, instance3_verify_info) - - #--------------------------------------------------------------- - # Add another service with TXT entries to the existing host and - # verify that it is properly merged. - - client3.srp_client_add_service('ins4', (SERVICE + ",_s1").upper(), 44444, 4, 4, txt_entries=['KEY=ABC']) - self.simulator.go(5) - - service_instances = client1.dns_browse(f'{SERVICE}.{DOMAIN}', server.get_mleid(), 53) - self.assertEqual({'ins1', 'ins2', 'ins3', 'ins4'}, set(service_instances.keys())) - - service_instance = client1.dns_resolve_service('ins4', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) - self._assert_service_instance_equal(service_instance, instance4_verify_info) - - #--------------------------------------------------------------- - # Query for KEY record for `ins1` service name - - records = client1.dns_query(25, 'ins1', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) - self.assertEqual(len(records), 1) - record = records[0] - self.assertEqual(int(record['RecordType']), 25) - self.assertEqual(int(record['RecordLength']), 78) - self.assertTrue(int(record['TTL']) > 0) - self.assertEqual(record['Section'], 'answer') - self.assertEqual(record['Name'].lower(), 'ins1._ipps._tcp.default.service.arpa.') - self.assertIn('RecordData', record) - - #--------------------------------------------------------------- - # Query for SRV record for `ins1` service name - - records = client1.dns_query(33, 'ins1', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) - self.assertEqual(len(records), 4) - - # SRV record in answer section - record = records[0] - self.assertEqual(int(record['RecordType']), 33) - self.assertTrue(int(record['RecordLength']) > 0) - self.assertTrue(int(record['TTL']) > 0) - self.assertEqual(record['Section'], 'answer') - self.assertEqual(record['Name'].lower(), 'ins1._ipps._tcp.default.service.arpa.') - self.assertIn('RecordData', record) - - # Other records TXT and A in additional section - for record in records[1:]: - self.assertTrue(int(record['RecordLength']) > 0) - self.assertIn('RecordData', record) - self.assertTrue(int(record['TTL']) > 0) - self.assertEqual(record['Section'], 'additional') - rrtype = int(record['RecordType']) - self.assertIn(rrtype, [16, 28]) # TXT and AAAA - if rrtype == 16: - self.assertEqual(record['Name'].lower(), 'ins1._ipps._tcp.default.service.arpa.') - self.assertEqual(record['RecordData'], '[00]') - else: - self.assertEqual(record['Name'].lower(), 'host1.default.service.arpa.') - - #--------------------------------------------------------------- - # Query for non-existing A record for `ins1` service name - - records = client1.dns_query(1, 'ins1', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) - self.assertEqual(len(records), 0) - - def _assert_service_instance_equal(self, instance, info): - self.assertEqual(instance['host'].lower(), info['host'].lower(), instance) - for f in ('port', 'priority', 'weight', 'txt_data'): - self.assertEqual(instance[f], info[f], instance) - - verify_addresses = info['address'] - if not isinstance(verify_addresses, typing.Collection): - verify_addresses = [verify_addresses] - self.assertIn(ipaddress.IPv6Address(instance['address']), map(ipaddress.IPv6Address, verify_addresses), - instance) - - for ttl_f in ('srv_ttl', 'txt_ttl', 'aaaa_ttl'): - check_ttl = info[ttl_f] - if not callable(check_ttl): - check_ttl = lambda x: x == check_ttl - - self.assertTrue(check_ttl(instance[ttl_f]), instance) - - def _config_srp_client_services(self, - client, - server, - instancename, - hostname, - port, - priority, - weight, - addrs, - subtypes=''): - client.srp_client_set_host_name(hostname) - client.srp_client_set_host_address(*addrs) - client.srp_client_add_service(instancename, SERVICE + subtypes, port, priority, weight) - - self.simulator.go(5) - self.assertEqual(client.srp_client_get_host_state(), 'Registered') - - -if __name__ == '__main__': - unittest.main()