[nexus] add test_srp_lease to validate SRP client/server interactions (#12588)

This commit adds a new Nexus test `test_srp_lease` to verify the
behavior of the SRP server and client regarding lease expirations and
state updates. The test creates a network with an SRP server and
multiple client nodes that randomly perform various SRP operations
including registering, updating, unregistering, and removing services,
as well as abruptly disconnecting. The test then validates that the
SRP server correctly updates the host and service states, such as
marking them as deleted or fully removing them when their leases
expire or when explicitly requested by the clients.
This commit is contained in:
Abtin Keshavarzian
2026-03-02 18:10:34 -06:00
committed by GitHub
parent 9682126aab
commit 9d3852db4f
2 changed files with 397 additions and 0 deletions
+1
View File
@@ -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")
+396
View File
@@ -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 <stdio.h>
#include <string.h>
#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<Srp::Client>().EnableAutoStartMode(nullptr, nullptr);
SuccessOrQuit(aNode.Get<Srp::Client>().EnableAutoHostAddress());
SuccessOrQuit(aNode.Get<Srp::Client>().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<Srp::Client>().AddService(*service));
}
static void UpdateSrpRegistration(Node &aNode)
{
NodeInfo *info;
Srp::Client::Service *service;
VerifyOrQuit(aNode.GetId() < kMaxNodes);
aNode.Get<Srp::Client>().ClearHostAndServices();
info = &sInfo[aNode.GetId()];
service = &info->mService;
SuccessOrQuit(aNode.Get<Srp::Client>().EnableAutoHostAddress());
SuccessOrQuit(aNode.Get<Srp::Client>().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<Srp::Client>().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<kNameStringSize> fullName;
fullName.Append("%s.default.service.arpa.", aName);
while (true)
{
host = aServer.Get<Srp::Server>().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<Srp::Server>().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<Mle::Mle>().IsLeader());
leader->Get<Srp::Server>().SetEnabled(true);
nexus.AdvanceTime(30 * Time::kOneSecondInMsec);
VerifyOrQuit(leader->Get<Srp::Server>().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<Mle::Mle>().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<Srp::Client>().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<Srp::Client>().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<Srp::Client>().RemoveHostAndServices(/* aRemoveKeyLease */ false));
info.SetState(kUnregistered, i);
}
else if (ShouldPerform(kRemoveProbability))
{
SuccessOrQuit(nodes[i]->Get<Srp::Client>().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;
}