diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index f1589f5a5..539228ee8 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -395,6 +395,7 @@ 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(dnssd_name_with_special_chars "core;nexus") ot_nexus_test(dtls "core;nexus") ot_nexus_test(form_join "core;nexus") ot_nexus_test(ipv6_fragmentation "core;nexus") diff --git a/tests/nexus/test_dnssd_name_with_special_chars.cpp b/tests/nexus/test_dnssd_name_with_special_chars.cpp new file mode 100644 index 000000000..467597a0d --- /dev/null +++ b/tests/nexus/test_dnssd_name_with_special_chars.cpp @@ -0,0 +1,229 @@ +/* + * 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 "net/dnssd_server.hpp" +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +/** + * Time to advance for a node to form a network and become leader, in milliseconds. + */ +static constexpr uint32_t kFormNetworkTime = 13 * 1000; + +/** + * Time to advance for a node to join a network, in milliseconds. + */ +static constexpr uint32_t kJoinNetworkTime = 10 * 1000; + +/** + * Time to advance for the network to stabilize, in milliseconds. + */ +static constexpr uint32_t kStabilizationTime = 15 * 1000; + +/** + * Time to wait for SRP registration to complete. + */ +static constexpr uint32_t kSrpRegistrationTime = 10 * 1000; + +/** + * Time to advance for DNS query processing. + */ +static constexpr uint32_t kDnsQueryTime = 5 * 1000; + +/** + * SRP service and host names. + */ +static const char kSrpServiceType[] = "_srv._udp"; +static const char kSrpFullServiceType[] = "_srv._udp.default.service.arpa."; +static const char kSrpHostName[] = "host1"; +static const char kSpecialInstanceName[] = "O\\T 网关"; +static constexpr uint16_t kSrpServicePort = 1977; + +/** + * Test context to track progress. + */ +struct TestContext +{ + uint8_t mBrowseCount; + uint8_t mResolveCount; +}; + +void HandleResolveResponse(otError aError, const otDnsServiceResponse *aResponse, void *aContext) +{ + TestContext *context = static_cast(aContext); + Dns::Client::ServiceInfo serviceInfo; + Dns::Name::Buffer hostName; + + if (aError != kErrorNone) + { + Log("HandleResolveResponse error: %s", ErrorToString(aError)); + } + SuccessOrQuit(aError); + + { + const Dns::Client::ServiceResponse &response = AsCoreType(aResponse); + + ClearAllBytes(serviceInfo); + serviceInfo.mHostNameBuffer = hostName; + serviceInfo.mHostNameBufferSize = sizeof(hostName); + + SuccessOrQuit(response.GetServiceInfo(serviceInfo)); + + VerifyOrQuit(serviceInfo.mPort == kSrpServicePort); + VerifyOrQuit(StringMatch(hostName, "host1.default.service.arpa.")); + } + + context->mResolveCount++; +} + +void HandleBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse, void *aContext) +{ + TestContext *context = static_cast(aContext); + Dns::Name::LabelBuffer label; + + if (aError != kErrorNone) + { + Log("HandleBrowseResponse error: %s", ErrorToString(aError)); + } + SuccessOrQuit(aError); + + { + const Dns::Client::BrowseResponse &response = AsCoreType(aResponse); + + for (uint16_t index = 0; response.GetServiceInstance(index, label, sizeof(label)) == kErrorNone; index++) + { + Log("Found service instance %s", label); + if (StringMatch(label, kSpecialInstanceName)) + { + context->mBrowseCount++; + } + } + } +} + +/** + * Verifies SRP/DNS client/server support for Service Instance names with special and unicode characters. + * + * Topology: + * SERVER (SRP server) + * | + * CLIENT (SRP client) + * + */ +void TestDnssdNameWithSpecialChars(void) +{ + Core nexus; + Node &server = nexus.CreateNode(); + Node &client = nexus.CreateNode(); + + server.SetName("SERVER"); + client.SetName("CLIENT"); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote)); + + Log("Forming network"); + server.Form(); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(server.Get().IsLeader()); + + client.Join(server); + nexus.AdvanceTime(kJoinNetworkTime); + VerifyOrQuit(client.Get().IsFullThreadDevice()); + + SuccessOrQuit(server.Get().SetAddressMode(Srp::Server::kAddressModeUnicast)); + server.Get().SetEnabled(true); + SuccessOrQuit(server.Get().Start()); + client.Get().EnableAutoStartMode(nullptr, nullptr); + nexus.AdvanceTime(kStabilizationTime); + + Log("Registering service with special characters: %s", kSpecialInstanceName); + SuccessOrQuit(client.Get().SetHostName(kSrpHostName)); + SuccessOrQuit(client.Get().EnableAutoHostAddress()); + + Srp::Client::Service service; + ClearAllBytes(service); + service.mName = kSrpServiceType; + service.mInstanceName = kSpecialInstanceName; + service.mPort = kSrpServicePort; + SuccessOrQuit(service.Init()); + SuccessOrQuit(client.Get().AddService(service)); + + nexus.AdvanceTime(kSrpRegistrationTime); + + { + Srp::Client::ItemState state = client.Get().GetHostInfo().GetState(); + Log("SRP Host State: %d", state); + VerifyOrQuit(state == Srp::Client::kRegistered); + } + + Log("Checking service discovery via DNS browse"); + TestContext context; + context.mBrowseCount = 0; + context.mResolveCount = 0; + + Dns::Client::QueryConfig queryConfig = client.Get().GetDefaultConfig(); + AsCoreType(&queryConfig.mServerSockAddr).SetAddress(server.Get().GetMeshLocalEid()); + queryConfig.mServerSockAddr.mPort = 53; + + SuccessOrQuit(client.Get().Browse(kSrpFullServiceType, HandleBrowseResponse, &context, &queryConfig)); + nexus.AdvanceTime(kDnsQueryTime); + + VerifyOrQuit(context.mBrowseCount == 1); + + Log("Checking service discovery via DNS resolve"); + SuccessOrQuit(client.Get().ResolveService(kSpecialInstanceName, kSrpFullServiceType, + HandleResolveResponse, &context, &queryConfig)); + nexus.AdvanceTime(kDnsQueryTime); + + VerifyOrQuit(context.mResolveCount == 1); + + Log("Checking service discovery via DNS resolve (case-insensitive)"); + // Note: DNS is generally case-insensitive for ASCII. + // Unicode case-insensitivity might be more complex, but let's see if it works. + SuccessOrQuit(client.Get().ResolveService("o\\t 网关", kSrpFullServiceType, HandleResolveResponse, + &context, &queryConfig)); + nexus.AdvanceTime(kDnsQueryTime); + + VerifyOrQuit(context.mResolveCount == 2); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestDnssdNameWithSpecialChars(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/thread-cert/test_dnssd_name_with_special_chars.py b/tests/scripts/thread-cert/test_dnssd_name_with_special_chars.py deleted file mode 100755 index 62c13e740..000000000 --- a/tests/scripts/thread-cert/test_dnssd_name_with_special_chars.py +++ /dev/null @@ -1,117 +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 unittest -import logging - -import command -import config -import thread_cert - -# Test description: -# This test verifies SRP/DNS client/server support for Service Instance names with special and unicode characters. -# -# Topology: -# -# LEADER (SRP server) -# | -# | -# ROUTER (SRP client) -# - -SERVER = 1 -CLIENT = 2 - -# TODO: support Instance names with dots (.) -SPECIAL_INSTANCE_NAME = 'O\\T 网关' - - -class TestDnssdNameWithSpecialChars(thread_cert.TestCase): - USE_MESSAGE_FACTORY = False - SUPPORT_NCP = False - - TOPOLOGY = { - SERVER: { - 'name': 'SERVER', - 'mode': 'rdn', - }, - CLIENT: { - 'name': 'CLIENT', - 'mode': 'rdn', - }, - } - - def test(self): - server = self.nodes[SERVER] - client = self.nodes[CLIENT] - - # Start the server & client devices. - - server.start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(server.get_state(), 'leader') - - client.start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - self.assertEqual(client.get_state(), 'router') - - server.srp_server_set_enabled(True) - client.srp_client_enable_auto_start_mode() - self.simulator.go(15) - - # Register a single service with the instance name containing special chars - client.srp_client_set_host_name('host1') - client.srp_client_set_host_address('2001::1') - client.srp_client_add_service(SPECIAL_INSTANCE_NAME, '_srv._udp', 1977) - self.simulator.go(5) - self.__check_service_discovery(server, client) - - def __check_service_discovery(self, server, client): - # Check the service on client - client.dns_set_config(server.get_rloc()) - services = client.dns_browse('_srv._udp.default.service.arpa') - self.assertEqual(1, len(services)) - self.assertIn(SPECIAL_INSTANCE_NAME, services) - self.__verify_service(services[SPECIAL_INSTANCE_NAME]) - - service = client.dns_resolve_service(SPECIAL_INSTANCE_NAME, '_srv._udp.default.service.arpa') - self.__verify_service(service) - - service = client.dns_resolve_service(SPECIAL_INSTANCE_NAME.lower(), '_srv._udp.default.service.arpa') - self.__verify_service(service) - - def __verify_service(self, service): - logging.info('service discovered: %r', service) - self.assertEqual(service['port'], 1977) - self.assertEqual(service['host'], 'host1.default.service.arpa.') - - -if __name__ == '__main__': - unittest.main()