From 8f5a9ff4b88b582d5e5c845bbaebe5e3e8cd07d5 Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Mon, 27 Apr 2026 13:02:17 -0700 Subject: [PATCH] [nexus] migrate srp server anycast test to nexus (#12951) This commit migrates the SRP server anycast mode test from the thread-cert Python script to the Nexus test framework. The new Nexus test `test_srp_server_anycast_mode.cpp` covers: - SRP Server configuration in both Anycast and Unicast modes. - Proper publication of SRP Server information in Network Data. - SRP Client auto-start and server selection logic. - Service registration and verification in both address modes. - DNS browsing for registered SRP services. Nexus tests allow for faster and more scalable network simulations within a single process, improving CI efficiency. Removed: - tests/scripts/thread-cert/test_srp_server_anycast_mode.py --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/test_srp_server_anycast_mode.cpp | 255 ++++++++++++++++++ .../test_srp_server_anycast_mode.py | 201 -------------- 3 files changed, 256 insertions(+), 201 deletions(-) create mode 100644 tests/nexus/test_srp_server_anycast_mode.cpp delete mode 100755 tests/scripts/thread-cert/test_srp_server_anycast_mode.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 2d6083067..a3e9ffd9d 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -419,6 +419,7 @@ ot_nexus_test(srp_lease "core;nexus") ot_nexus_test(srp_many_services_mtu_check "core;nexus") ot_nexus_test(srp_register_services_diff_lease "core;nexus") ot_nexus_test(srp_scale "core;nexus") +ot_nexus_test(srp_server_anycast_mode "core;nexus") ot_nexus_test(srp_server_reboot_port "core;nexus") ot_nexus_test(srp_ttl "core;nexus") ot_nexus_test(zero_len_external_route "core;nexus") diff --git a/tests/nexus/test_srp_server_anycast_mode.cpp b/tests/nexus/test_srp_server_anycast_mode.cpp new file mode 100644 index 000000000..2ff32e2ce --- /dev/null +++ b/tests/nexus/test_srp_server_anycast_mode.cpp @@ -0,0 +1,255 @@ +/* + * 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 "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 SRP server information to be updated in Network Data. + */ +static constexpr uint32_t kSrpServerInfoUpdateTime = 20 * 1000; + +/** + * Time to advance for SRP registration, in milliseconds. + */ +static constexpr uint32_t kSrpRegistrationTime = 15 * 1000; + +/** + * Time to advance for DNS query processing, in milliseconds. + */ +static constexpr uint32_t kDnsQueryTime = 5 * 1000; + +/** + * SRP service registration parameters. + */ +static const char kSrpServiceType[] = "_srv._udp"; +static const char kSrpInstanceName[] = "ins1"; +static const char kSrpHostName[] = "host"; +static const char kSrpFullServiceType[] = "_srv._udp.default.service.arpa."; +static constexpr uint16_t kSrpServicePort = 1313; + +static constexpr uint8_t kSrpServerAnycastSeqNum = 17; +static constexpr uint16_t kSrpServerAnycastPort = 53; + +/** + * Test context for DNS browse resolution. + */ +struct BrowseContext +{ + Node *mClientNode; + bool mDone; +}; + +static void HandleBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse, void *aContext) +{ + BrowseContext *context = static_cast(aContext); + const Dns::Client::BrowseResponse &response = *static_cast(aResponse); + char label[Dns::Name::kMaxLabelSize]; + Dns::Client::ServiceInfo serviceInfo; + char hostName[Dns::Name::kMaxNameSize]; + + SuccessOrQuit(aError); + + // Since there is only one match the server should include the service info in additional section. + SuccessOrQuit(response.GetServiceInstance(0, label, sizeof(label))); + VerifyOrQuit(StringMatch(label, kSrpInstanceName, kStringCaseInsensitiveMatch)); + + serviceInfo.mHostNameBuffer = hostName; + serviceInfo.mHostNameBufferSize = sizeof(hostName); + serviceInfo.mTxtData = nullptr; + serviceInfo.mTxtDataSize = 0; + + SuccessOrQuit(response.GetServiceInfo(label, serviceInfo)); + + VerifyOrQuit(serviceInfo.mPort == kSrpServicePort); + VerifyOrQuit(StringStartsWith(serviceInfo.mHostNameBuffer, kSrpHostName, kStringCaseInsensitiveMatch)); + VerifyOrQuit(AsCoreType(&serviceInfo.mHostAddress) == context->mClientNode->Get().GetMeshLocalEid()); + + context->mDone = true; +} + +void TestSrpServerAnycastMode(const char *aJsonFileName) +{ + // This test covers the SRP server's behavior using different address + // modes (unicast or anycast). The address mode indicates how the SRP + // server determines its address and port and how this info is + // published in the Thread Network Data. When using anycast address + // mode, the SRP server and DNS server/resolver will both listen on port + // 53 and they re-use the same socket. This test verifies the behavior of + // both modules in such a situation. + + Core nexus; + + Node &server = nexus.CreateNode(); + Node &client = nexus.CreateNode(); + Node &browser = nexus.CreateNode(); + + server.SetName("SERVER"); + client.SetName("CLIENT"); + browser.SetName("BROWSER"); + + server.Form(); + nexus.AdvanceTime(0); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote)); + + Log("---------------------------------------------------------------------------------------"); + Log("Test SRP Server Anycast and Unicast modes"); + + nexus.AdvanceTime(kFormNetworkTime); + + client.Join(server); + browser.Join(server); + nexus.AdvanceTime(kJoinNetworkTime); + + const Srp::Server::AddressMode kAddressModes[] = {Srp::Server::kAddressModeAnycast, + Srp::Server::kAddressModeUnicast}; + + for (Srp::Server::AddressMode addrMode : kAddressModes) + { + Log("Address Mode: %s", (addrMode == Srp::Server::kAddressModeAnycast) ? "anycast" : "unicast"); + + // 1. Set the SRP server address mode and start the SRP server. + SuccessOrQuit(server.Get().SetAddressMode(addrMode)); + + if (addrMode == Srp::Server::kAddressModeAnycast) + { + SuccessOrQuit(server.Get().SetAnycastModeSequenceNumber(kSrpServerAnycastSeqNum)); + VerifyOrQuit(server.Get().GetAnycastModeSequenceNumber() == kSrpServerAnycastSeqNum); + } + + server.Get().SetEnabled(true); + nexus.AdvanceTime(kSrpServerInfoUpdateTime); + + // Verify that the SRP server information is correctly published in Network Data. + { + NetworkData::Service::Iterator iterator(client.Get()); + + if (addrMode == Srp::Server::kAddressModeAnycast) + { + NetworkData::Service::DnsSrpAnycastInfo anycastInfo; + + SuccessOrQuit(iterator.GetNextDnsSrpAnycastInfo(anycastInfo)); + VerifyOrQuit(anycastInfo.mSequenceNumber == kSrpServerAnycastSeqNum); + VerifyOrQuit(iterator.GetNextDnsSrpAnycastInfo(anycastInfo) == kErrorNotFound); + } + else + { + NetworkData::Service::DnsSrpUnicastInfo unicastInfo; + + SuccessOrQuit(iterator.GetNextDnsSrpUnicastInfo(NetworkData::Service::kAddrInServerData, unicastInfo)); + VerifyOrQuit(unicastInfo.mSockAddr.GetAddress() == server.Get().GetMeshLocalEid()); + VerifyOrQuit(unicastInfo.mSockAddr.GetPort() == server.Get().GetPort()); + VerifyOrQuit(iterator.GetNextDnsSrpUnicastInfo(NetworkData::Service::kAddrInServerData, unicastInfo) == + kErrorNotFound); + } + } + + // 2. Enable auto-start on SRP client and browser. + client.Get().EnableAutoStartMode(nullptr, nullptr); + browser.Get().EnableAutoStartMode(nullptr, nullptr); + nexus.AdvanceTime(kSrpRegistrationTime); + + // 3. Verify client finds the server and uses proper address/port. + if (addrMode == Srp::Server::kAddressModeAnycast) + { + // In anycast mode, the client should find the Anycast ALOC address and port 53. + VerifyOrQuit(client.Get().GetServerAddress().GetPort() == kSrpServerAnycastPort); + const Ip6::Address &serverAddr = client.Get().GetServerAddress().GetAddress(); + VerifyOrQuit(serverAddr.GetIid().IsAnycastServiceLocator()); + } + else + { + // In unicast mode, it uses ML-EID and a dynamic port. + VerifyOrQuit(client.Get().GetServerAddress().GetAddress() == + server.Get().GetMeshLocalEid()); + VerifyOrQuit(client.Get().GetServerAddress().GetPort() == server.Get().GetPort()); + } + + // 4. Add a service on SRP client and verify registration. + SuccessOrQuit(client.Get().SetHostName(kSrpHostName)); + SuccessOrQuit(client.Get().EnableAutoHostAddress()); + + Srp::Client::Service service; + ClearAllBytes(service); + service.mName = kSrpServiceType; + service.mInstanceName = kSrpInstanceName; + service.mPort = kSrpServicePort; + SuccessOrQuit(client.Get().AddService(service)); + + nexus.AdvanceTime(kSrpRegistrationTime); + + // Check if service is registered on client side. + VerifyOrQuit(service.GetState() == Srp::Client::kRegistered); + + // 5. Browse for the service from the browser node and verify result. + BrowseContext context; + context.mClientNode = &client; + context.mDone = false; + + SuccessOrQuit(browser.Get().Browse(kSrpFullServiceType, HandleBrowseResponse, &context)); + nexus.AdvanceTime(kDnsQueryTime); + VerifyOrQuit(context.mDone); + + // 6. Clear host on client and stop both client and server. + client.Get().ClearHostAndServices(); + client.Get().Stop(); + server.Get().SetEnabled(false); + nexus.AdvanceTime(kSrpServerInfoUpdateTime); + } + + Log("All steps completed."); + nexus.SaveTestInfo(aJsonFileName); +} + +} // namespace Nexus +} // namespace ot + +int main(int argc, char *argv[]) +{ + ot::Nexus::TestSrpServerAnycastMode((argc > 2) ? argv[2] : "test_srp_server_anycast_mode.json"); + + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/thread-cert/test_srp_server_anycast_mode.py b/tests/scripts/thread-cert/test_srp_server_anycast_mode.py deleted file mode 100755 index 7bae2e72b..000000000 --- a/tests/scripts/thread-cert/test_srp_server_anycast_mode.py +++ /dev/null @@ -1,201 +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 unittest - -import config -import thread_cert - -# Test description: -# -# This test covers the SRP server's behavior using different address -# modes (unicast or anycast). The address mode indicates how the SRP -# server determines its address and port and how this info is -# published in the Thread Network Data. When using anycast address -# mode, the SRP server and DNS server/resolver will both listen on port -# 53 and they re-use the same socket. This test verifies the behavior of -# both modules in such a situation. -# -# -# Topology: -# -# One leader and two router nodes, all connected. The leader acts as SRP -# server and DNS resolver. One router acts as SRP client, and the -# other acts as DNS client browsing for registered service names. -# - -SERVER = 1 -CLIENT = 2 -BROWSER = 3 - -DOMAIN = 'default.service.arpa.' -HOST = 'host' -INSTANCE = 'ins1' -SERVICE = '_srv._udp' -SERVICE_PORT = 1313 - -SRP_SERVER_ANYCAST_PORT = 53 -SRP_SERVER_ANYCAST_SEQ_NUM = 17 -DNS_RESOLVER_PORT = 53 - -THREAD_ENTERPRISE_NUMBER = 44970 -ANYCAST_SERVICE_NUM = 0x5c -UNICAST_SERVICE_NUM = 0x5d - - -class TestSrpServerAnycastMode(thread_cert.TestCase): - SUPPORT_NCP = False - USE_MESSAGE_FACTORY = False - - TOPOLOGY = { - SERVER: { - 'mode': 'rdn', - }, - CLIENT: { - 'mode': 'rdn', - }, - BROWSER: { - 'mode': 'rdn', - }, - } - - def test(self): - server = self.nodes[SERVER] - client = self.nodes[CLIENT] - browser = self.nodes[BROWSER] - - #------------------------------------------------------------------- - # Form the network. - - server.start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(server.get_state(), 'leader') - - client.start() - browser.start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - self.assertEqual(client.get_state(), 'router') - self.assertEqual(browser.get_state(), 'router') - - #------------------------------------------------------------------- - # Go through the entire test twice, first time using anycast address - # mode and second time using unicast address mode. - - for addr_mode in ['anycast', 'unicast']: - - #--------------------------------------------------------------- - # Set the SRP server address mode and start the SRP server. - - server.srp_server_set_addr_mode(addr_mode) - - if addr_mode == 'anycast': - server.srp_server_set_anycast_seq_num(SRP_SERVER_ANYCAST_SEQ_NUM) - self.assertEqual(server.srp_server_get_anycast_seq_num(), SRP_SERVER_ANYCAST_SEQ_NUM) - - self.assertEqual(server.srp_server_get_addr_mode(), addr_mode) - server.srp_server_set_enabled(True) - self.simulator.go(5) - - #--------------------------------------------------------------- - # Verify the published SRP server info in the Network Data. - - netdata_services = client.get_services() - self.assertEqual(len(netdata_services), 1) - netdata_service = netdata_services[0] - - self.assertEqual(int(netdata_service[0]), THREAD_ENTERPRISE_NUMBER) - data = bytes.fromhex(netdata_service[1]) - self.assertEqual(netdata_service[3], 's') - - if addr_mode == 'anycast': - self.assertTrue(len(data) >= 2) - self.assertEqual(data[0], ANYCAST_SERVICE_NUM) - self.assertEqual(data[1], SRP_SERVER_ANYCAST_SEQ_NUM) - else: - self.assertTrue(len(data) >= 1) - self.assertEqual(data[0], UNICAST_SERVICE_NUM) - self.assertEqual(netdata_service[3], 's') - - #--------------------------------------------------------------- - # Enable auto-start on SRP client. Verify that it does find the - # server and uses the proper address and port number. - - client.srp_client_enable_auto_start_mode() - self.simulator.go(15) - - if addr_mode == 'anycast': - server_alocs = server.get_ip6_address(config.ADDRESS_TYPE.ALOC) - self.assertEqual(client.srp_client_get_state(), 'Enabled') - self.assertIn(client.srp_client_get_server_address(), server_alocs) - self.assertEqual(client.srp_client_get_server_port(), SRP_SERVER_ANYCAST_PORT) - else: - self.assertIn(client.srp_client_get_server_address(), server.get_mleid()) - self.assertEqual(client.srp_client_get_server_port(), server.get_srp_server_port()) - - #--------------------------------------------------------------- - # Add a service on the SRP client and verify its successful - # registration with SRP server. - - client.srp_client_set_host_name(HOST) - client.srp_client_set_host_address(client.get_mleid()) - client.srp_client_add_service(INSTANCE, SERVICE, SERVICE_PORT) - self.simulator.go(5) - - client_services = client.srp_client_get_services() - self.assertEqual(len(client_services), 1) - client_service = client_services[0] - self.assertEqual(client_service['instance'], INSTANCE) - self.assertEqual(client_service['name'], SERVICE) - self.assertEqual(int(client_service['port']), SERVICE_PORT) - self.assertEqual(client_service['state'], 'Registered') - - #--------------------------------------------------------------- - # Browse for a matching service name and verify that the registered - # service is successfully found. Since there is only one match the - # server should include the service info in additional section. - - service_instances = browser.dns_browse(f'{SERVICE}.{DOMAIN}', server.get_mleid(), DNS_RESOLVER_PORT) - self.assertEqual({INSTANCE}, set(service_instances.keys())) - - service_instance = service_instances[INSTANCE] - self.assertEqual(service_instance['host'], f'{HOST}.{DOMAIN}') - self.assertEqual(int(service_instance['port']), SERVICE_PORT) - self.assertEqual(service_instance['address'], client.get_mleid()) - - #--------------------------------------------------------------- - # Stop SRP client and server and clear host (and service) on the - # client. - - client.srp_client_clear_host() - client.srp_client_stop() - server.srp_server_set_enabled(False) - - -if __name__ == '__main__': - unittest.main()