diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 5d745df58..6d6f9e538 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -223,6 +223,7 @@ ot_nexus_test(discover_scan "core;nexus") ot_nexus_test(dtls "core;nexus") ot_nexus_test(form_join "core;nexus") ot_nexus_test(nat64_translator "core;nexus") +ot_nexus_test(srp_lease "core;nexus") # Trel ot_nexus_test(trel "trel;nexus") diff --git a/tests/nexus/test_srp_lease.cpp b/tests/nexus/test_srp_lease.cpp new file mode 100644 index 000000000..a49c766b6 --- /dev/null +++ b/tests/nexus/test_srp_lease.cpp @@ -0,0 +1,396 @@ +/* + * 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 { + +static const uint16_t kNumNodes[] = {1, 5, 10}; // Number of nodes to use in different test runs + +constexpr uint16_t kTestIterationsPerNumNodes = 1; // Number of iterations to run per num-node + +enum State : uint8_t +{ + kNotRegistered, // No SRP registration yet + kRegistered, // SRP service registered + kUnregistered, // Unregistered the previously registered service (retaining name) + kRemoved, // Fully removed host and service (not retaining key-lease) + kDisconnected, // Disconnected (checking if the server correctly expires the previously registered service) + kValidated, // Final state of all nodes indicating validated server behavior +}; + +constexpr uint16_t kMaxNodes = 40; + +// Test advances time in `kStepInterval` +constexpr uint32_t kStepInterval = 120 * TimeMilli::kOneSecondInMsec; +constexpr uint16_t kStepJitter = 10 * TimeMilli::kOneSecondInMsec; + +constexpr uint32_t kLeaseInterval = 7200 * TimeMilli::kOneSecondInMsec; + +// All probabilities are percentage times 10 +constexpr uint16_t kRegisterProbability = 8; +constexpr uint16_t kUpdateRegProbability = 8; +constexpr uint16_t kUnregisterProbability = 1; +constexpr uint16_t kRemoveProbability = 1; +constexpr uint16_t kDisconnectProbability = 4; + +static const char *StateToString(State aState) +{ +#define StateMapList(_) \ + _(kNotRegistered, "NotRegistered") \ + _(kRegistered, "Registered") \ + _(kUnregistered, "Unregistered") \ + _(kRemoved, "Removed") \ + _(kDisconnected, "Disconnected") \ + _(kValidated, "Validated") + + DefineEnumStringArray(StateMapList); + + return kStrings[aState]; +} + +struct NodeInfo +{ + void SetState(State aState, uint16_t aNodeId) + { + Log("Node %u: %s", aNodeId, StateToString(aState)); + + mState = aState; + mUpdateTime = TimerMilli::GetNow(); + } + + State mState; + TimeMilli mUpdateTime; + Srp::Client::Service mService; + const char *mSubTypeLabels[5]; +}; + +NodeInfo sInfo[kMaxNodes]; + +static void RegisterSrp(Node &aNode) +{ + NodeInfo *info; + Srp::Client::Service *service; + + VerifyOrQuit(aNode.GetId() < kMaxNodes); + + info = &sInfo[aNode.GetId()]; + service = &info->mService; + + aNode.Get().EnableAutoStartMode(nullptr, nullptr); + + SuccessOrQuit(aNode.Get().EnableAutoHostAddress()); + SuccessOrQuit(aNode.Get().SetHostName(aNode.GetName())); + + ClearAllBytes(*service); + service->mName = "_test._udp"; + service->mInstanceName = aNode.GetName(); + service->mSubTypeLabels = nullptr; + service->mTxtEntries = nullptr; + service->mNumTxtEntries = 0; + service->mPort = 1000 + aNode.GetId(); + + SuccessOrQuit(aNode.Get().AddService(*service)); +} + +static void UpdateSrpRegistration(Node &aNode) +{ + NodeInfo *info; + Srp::Client::Service *service; + + VerifyOrQuit(aNode.GetId() < kMaxNodes); + + aNode.Get().ClearHostAndServices(); + + info = &sInfo[aNode.GetId()]; + service = &info->mService; + + SuccessOrQuit(aNode.Get().EnableAutoHostAddress()); + SuccessOrQuit(aNode.Get().SetHostName(aNode.GetName())); + + ClearAllBytes(*service); + service->mName = "_test._udp"; + service->mInstanceName = aNode.GetName(); + service->mSubTypeLabels = nullptr; + service->mTxtEntries = nullptr; + service->mNumTxtEntries = 0; + service->mPort = Random::NonCrypto::GetUint16InRange(0x100, 0xff00); + service->mSubTypeLabels = &info->mSubTypeLabels[Random::NonCrypto::GetUint8InRange(0, 4)]; + + SuccessOrQuit(aNode.Get().AddService(*service)); +} + +static bool ShouldPerform(uint16_t aProbability) +{ + // Uses the given probability to randomly decide whether a certain action should be performed. + + return Random::NonCrypto::GetUint16InRange(0, 1000) < aProbability; +} + +static const Srp::Server::Host *FindHost(Node &aServer, const char *aName) +{ + // Find a host on `aServer` SRP server matching `aName` + + static constexpr uint16_t kNameStringSize = 400; + + const Srp::Server::Host *host = nullptr; + String fullName; + + fullName.Append("%s.default.service.arpa.", aName); + + while (true) + { + host = aServer.Get().GetNextHost(host); + + if (host == nullptr) + { + break; + } + + if (StringMatch(host->GetFullName(), fullName.AsCString())) + { + break; + } + } + + return host; +} + +void LogSrpServer(Node &aServer) +{ + const Srp::Server::Host *host = nullptr; + + Log("- - - - - -"); + + while (true) + { + host = aServer.Get().GetNextHost(host); + + if (host == nullptr) + { + break; + } + + Log("%s, is-deleted:%s", host->GetFullName(), ToYesNo(host->IsDeleted())); + } +} + +void TestSrpLease(uint16_t aNumNodes) +{ + Core nexus; + Node *nodes[kMaxNodes]; + Node *leader; + + VerifyOrQuit(aNumNodes + 1 <= kMaxNodes); + + Log("----------------------------------------------------------------------------------------------------------"); + Log("Testing with %u nodes", aNumNodes); + + leader = &nexus.CreateNode(); + + leader->Form(); + nexus.AdvanceTime(100 * Time::kOneSecondInMsec); + VerifyOrQuit(leader->Get().IsLeader()); + + leader->Get().SetEnabled(true); + nexus.AdvanceTime(30 * Time::kOneSecondInMsec); + + VerifyOrQuit(leader->Get().GetState() == Srp::Server::kStateRunning); + + nodes[0] = leader; + + for (uint16_t i = 1; i <= aNumNodes; i++) + { + sInfo[i].mState = kNotRegistered; + + sInfo[i].mSubTypeLabels[0] = "_s1"; + sInfo[i].mSubTypeLabels[1] = "_xvab"; + sInfo[i].mSubTypeLabels[2] = "_I12345678"; + sInfo[i].mSubTypeLabels[3] = "_cm"; + sInfo[i].mSubTypeLabels[4] = nullptr; + + nodes[i] = &nexus.CreateNode(); + + nodes[i]->SetName("client", i); + nodes[i]->Join(*leader, Node::kAsFed); + nexus.AdvanceTime(100); + } + + nexus.AdvanceTime(10 * Time::kOneSecondInMsec); + + for (uint16_t i = 1; i <= aNumNodes; i++) + { + VerifyOrQuit(nodes[i]->Get().IsChild()); + } + + while (true) + { + uint16_t nodesValidated = 0; + + nexus.AdvanceTime(Random::NonCrypto::AddJitter(kStepInterval, kStepJitter)); + + // LogSrpServer(*leader); + + for (uint16_t i = 1; i <= aNumNodes; i++) + { + NodeInfo &info = sInfo[i]; + const Srp::Server::Host *host; + + switch (info.mState) + { + case kNotRegistered: + if (ShouldPerform(kRegisterProbability)) + { + RegisterSrp(*nodes[i]); + nexus.AdvanceTime(1000); + VerifyOrQuit(nodes[i]->Get().GetHostInfo().GetState() == Srp::Client::kRegistered); + info.SetState(kRegistered, i); + } + + break; + + case kRegistered: + host = FindHost(*leader, nodes[i]->GetName()); + VerifyOrQuit(host != nullptr); + VerifyOrQuit(!host->IsDeleted()); + VerifyOrQuit(!host->GetServices().IsEmpty()); + VerifyOrQuit(!host->GetServices().GetHead()->IsDeleted()); + + if (ShouldPerform(kUpdateRegProbability)) + { + UpdateSrpRegistration(*nodes[i]); + nexus.AdvanceTime(1000); + VerifyOrQuit(nodes[i]->Get().GetHostInfo().GetState() == Srp::Client::kRegistered); + Log("Node %u: Updated SRP", i); + } + else if (ShouldPerform(kDisconnectProbability)) + { + nodes[i]->Reset(); + info.SetState(kDisconnected, i); + } + else if (ShouldPerform(kUnregisterProbability)) + { + SuccessOrQuit(nodes[i]->Get().RemoveHostAndServices(/* aRemoveKeyLease */ false)); + info.SetState(kUnregistered, i); + } + else if (ShouldPerform(kRemoveProbability)) + { + SuccessOrQuit(nodes[i]->Get().RemoveHostAndServices(/* aRemoveKeyLease */ true)); + info.SetState(kRemoved, i); + } + break; + + case kUnregistered: + // If the client unregistered (retaining the key-lease), the `host` + // on the server must be marked as deleted. + + host = FindHost(*leader, nodes[i]->GetName()); + VerifyOrQuit(host != nullptr); + VerifyOrQuit(host->IsDeleted()); + if (!host->GetServices().IsEmpty()) + { + VerifyOrQuit(host->GetServices().GetHead()->IsDeleted()); + } + + info.SetState(kValidated, i); + break; + + case kRemoved: + // If the client fully removes the registration, the `host` on + // the server must be fully removed. + + host = FindHost(*leader, nodes[i]->GetName()); + VerifyOrQuit(host == nullptr); + info.SetState(kValidated, i); + break; + + case kDisconnected: + // If the client disconnects without changing its registration, + // we ensure the `host` on the server is marked as deleted once + // its lease time expires. + + host = FindHost(*leader, nodes[i]->GetName()); + + if (host != nullptr) + { + if (host->IsDeleted()) + { + VerifyOrQuit(!host->GetServices().IsEmpty()); + VerifyOrQuit(host->GetServices().GetHead()->IsDeleted()); + info.SetState(kValidated, i); + } + else + { + if (info.mUpdateTime + kLeaseInterval < TimerMilli::GetNow()) + { + Log("Node %u is still on server beyond its lease time", i); + VerifyOrQuit(false); + } + } + } + else + { + info.SetState(kValidated, i); + } + + break; + + case kValidated: + nodesValidated++; + } + } + + if (nodesValidated == aNumNodes) + { + Log("Test passed successfully"); + break; + } + } +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + for (uint16_t numNodes : ot::Nexus::kNumNodes) + { + for (uint16_t iter = 0; iter < ot::Nexus::kTestIterationsPerNumNodes; iter++) + { + ot::Nexus::TestSrpLease(numNodes); + } + } + + printf("All tests passed\n"); + return 0; +}