blob: d5c6a173531806f7b953594dcf27b0bcab75477d [file]
/*
* Copyright (c) 2020, 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 "srp_client.hpp"
#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
#include "instance/instance.hpp"
/**
* @file
* This file implements the SRP client.
*/
namespace ot {
namespace Srp {
RegisterLogModule("SrpClient");
//---------------------------------------------------------------------
// Client::HostInfo
void Client::HostInfo::Init(void)
{
Clearable<HostInfo>::Clear();
// State is directly set on `mState` instead of using `SetState()`
// to avoid logging.
mState = OT_SRP_CLIENT_ITEM_STATE_REMOVED;
}
void Client::HostInfo::Clear(void)
{
Clearable<HostInfo>::Clear();
SetState(kRemoved);
}
bool Client::HostInfo::SetState(ItemState aState)
{
bool didChange;
VerifyOrExit(aState != GetState(), didChange = false);
LogInfo("HostInfo %s -> %s", ItemStateToString(GetState()), ItemStateToString(aState));
mState = MapEnum(aState);
didChange = true;
exit:
return didChange;
}
void Client::HostInfo::EnableAutoAddress(void)
{
mAddresses = nullptr;
mNumAddresses = 0;
mAutoAddress = true;
LogInfo("HostInfo enabled auto address");
}
void Client::HostInfo::SetAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses)
{
mAddresses = aAddresses;
mNumAddresses = aNumAddresses;
mAutoAddress = false;
LogInfo("HostInfo set %d addrs", GetNumAddresses());
for (uint8_t index = 0; index < GetNumAddresses(); index++)
{
LogInfo("%s", GetAddress(index).ToString().AsCString());
}
}
//---------------------------------------------------------------------
// Client::Service
Error Client::Service::Init(void)
{
Error error = kErrorNone;
VerifyOrExit((GetName() != nullptr) && (GetInstanceName() != nullptr), error = kErrorInvalidArgs);
VerifyOrExit((GetTxtEntries() != nullptr) || (GetNumTxtEntries() == 0), error = kErrorInvalidArgs);
// State is directly set on `mState` instead of using `SetState()`
// to avoid logging.
mState = OT_SRP_CLIENT_ITEM_STATE_REMOVED;
mLease = Min(mLease, kMaxLease);
mKeyLease = Min(mKeyLease, kMaxLease);
exit:
return error;
}
bool Client::Service::SetState(ItemState aState)
{
bool didChange;
VerifyOrExit(GetState() != aState, didChange = false);
LogInfo("Service %s -> %s, \"%s\" \"%s\"", ItemStateToString(GetState()), ItemStateToString(aState),
GetInstanceName(), GetName());
if (aState == kToAdd)
{
constexpr uint16_t kSubTypeLabelStringSize = 80;
String<kSubTypeLabelStringSize> string;
// Log more details only when entering `kToAdd` state.
if (HasSubType())
{
const char *label;
for (uint16_t index = 0; (label = GetSubTypeLabelAt(index)) != nullptr; index++)
{
string.Append("%s\"%s\"", (index != 0) ? ", " : "", label);
}
}
LogInfo("subtypes:[%s] port:%d weight:%d prio:%d txts:%d", string.AsCString(), GetPort(), GetWeight(),
GetPriority(), GetNumTxtEntries());
}
mState = MapEnum(aState);
didChange = true;
exit:
return didChange;
}
bool Client::Service::Matches(const Service &aOther) const
{
// This method indicates whether or not two service entries match,
// i.e., have the same service and instance names. This is intended
// for use by `LinkedList::FindMatching()` to search within the
// `mServices` list.
return StringMatch(GetName(), aOther.GetName()) && StringMatch(GetInstanceName(), aOther.GetInstanceName());
}
//---------------------------------------------------------------------
// Client::TxJitter
const uint32_t Client::TxJitter::kMaxJitters[] = {
Client::kMaxTxJitterOnDeviceReboot, // (0) kOnDeviceReboot
Client::kMaxTxJitterOnServerStart, // (1) kOnServerStart
Client::kMaxTxJitterOnServerRestart, // (2) kOnServerRestart
Client::kMaxTxJitterOnServerSwitch, // (3) kOnServerSwitch
Client::kMaxTxJitterOnSlaacAddrAdd, // (4) kOnSlaacAddrAdd
Client::kMaxTxJitterOnSlaacAddrRemove, // (5) kOnSlaacAddrRemove
};
void Client::TxJitter::Request(Reason aReason)
{
struct EnumCheck
{
InitEnumValidatorCounter();
ValidateNextEnum(kOnDeviceReboot);
ValidateNextEnum(kOnServerStart);
ValidateNextEnum(kOnServerRestart);
ValidateNextEnum(kOnServerSwitch);
ValidateNextEnum(kOnSlaacAddrAdd);
ValidateNextEnum(kOnSlaacAddrRemove);
};
uint32_t maxJitter = kMaxJitters[aReason];
LogInfo("Requesting max tx jitter %lu (%s)", ToUlong(maxJitter), ReasonToString(aReason));
if (mRequestedMax != 0)
{
// If we have a previous request, adjust the `mRequestedMax`
// based on the time elapsed since that request was made.
uint32_t duration = TimerMilli::GetNow() - mRequestTime;
mRequestedMax = (mRequestedMax > duration) ? mRequestedMax - duration : 0;
}
mRequestedMax = Max(mRequestedMax, maxJitter);
mRequestTime = TimerMilli::GetNow();
}
uint32_t Client::TxJitter::DetermineDelay(void)
{
uint32_t delay;
uint32_t maxJitter = kMaxTxJitterDefault;
if (mRequestedMax != 0)
{
uint32_t duration = TimerMilli::GetNow() - mRequestTime;
if (duration >= mRequestedMax)
{
LogInfo("Requested max tx jitter %lu already expired", ToUlong(mRequestedMax));
}
else
{
maxJitter = Max(mRequestedMax - duration, kMaxTxJitterDefault);
LogInfo("Applying remaining max jitter %lu", ToUlong(maxJitter));
}
mRequestedMax = 0;
}
delay = Random::NonCrypto::GetUint32InRange(kMinTxJitter, maxJitter);
LogInfo("Use random tx jitter %lu from [%lu, %lu]", ToUlong(delay), ToUlong(kMinTxJitter), ToUlong(maxJitter));
return delay;
}
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
const char *Client::TxJitter::ReasonToString(Reason aReason)
{
#define ReasonMapList(_) \
_(kOnDeviceReboot, "OnDeviceReboot") \
_(kOnServerStart, "OnServerStart") \
_(kOnServerRestart, "OnServerRestart") \
_(kOnServerSwitch, "OnServerSwitch") \
_(kOnSlaacAddrAdd, "OnSlaacAddrAdd") \
_(kOnSlaacAddrRemove, "OnSlaacAddrRemove")
DefineEnumStringArray(ReasonMapList);
return kStrings[aReason];
}
#endif
//---------------------------------------------------------------------
// Client::AutoStart
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
Client::AutoStart::AutoStart(void)
{
Clear();
mState = kDefaultMode ? kFirstTimeSelecting : kDisabled;
}
bool Client::AutoStart::HasSelectedServer(void) const
{
bool hasSelected = false;
switch (mState)
{
case kDisabled:
case kFirstTimeSelecting:
case kReselecting:
break;
case kSelectedUnicastPreferred:
case kSelectedUnicast:
case kSelectedAnycast:
hasSelected = true;
break;
}
return hasSelected;
}
void Client::AutoStart::SetState(State aState)
{
if (mState != aState)
{
LogInfo("AutoStartState %s -> %s", StateToString(mState), StateToString(aState));
mState = aState;
}
}
void Client::AutoStart::InvokeCallback(const Ip6::SockAddr *aServerSockAddr) const
{
mCallback.InvokeIfSet(aServerSockAddr);
}
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
const char *Client::AutoStart::StateToString(State aState)
{
#define AutoStartStateMapList(_) \
_(kDisabled, "Disabled") \
_(kFirstTimeSelecting, "1stTimeSelect") \
_(kReselecting, "Reselect") \
_(kSelectedUnicastPreferred, "Unicast-prf") \
_(kSelectedAnycast, "Anycast") \
_(kSelectedUnicast, "Unicast")
DefineEnumStringArray(AutoStartStateMapList);
return kStrings[aState];
}
#endif
#endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
//---------------------------------------------------------------------
// Client
const char Client::kDefaultDomainName[] = "default.service.arpa";
Client::Client(Instance &aInstance)
: InstanceLocator(aInstance)
, mState(kStateStopped)
, mTxFailureRetryCount(0)
, mShouldRemoveKeyLease(false)
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
, mServiceKeyRecordEnabled(false)
, mUseShortLeaseOption(false)
#endif
, mCurMessageId(0)
, mAutoHostAddressCount(0)
, mRetryWaitInterval(kMinRetryWaitInterval)
, mTtl(0)
, mLease(0)
, mKeyLease(0)
, mDefaultLease(kDefaultLease)
, mDefaultKeyLease(kDefaultKeyLease)
, mSocket(aInstance, *this)
, mDomainName(kDefaultDomainName)
, mTimer(aInstance)
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
, mGuardTimer(aInstance)
#endif
{
// The `Client` implementation uses different constant array of
// `ItemState` to define transitions between states in `Pause()`,
// `Stop()`, `SendUpdate`, and `ProcessResponse()`, or to convert
// an `ItemState` to string. Here, we assert that the enumeration
// values are correct.
struct EnumCheck
{
InitEnumValidatorCounter();
ValidateNextEnum(kToAdd);
ValidateNextEnum(kAdding);
ValidateNextEnum(kToRefresh);
ValidateNextEnum(kRefreshing);
ValidateNextEnum(kToRemove);
ValidateNextEnum(kRemoving);
ValidateNextEnum(kRegistered);
ValidateNextEnum(kRemoved);
};
mHostInfo.Init();
}
Error Client::Start(const Ip6::SockAddr &aServerSockAddr, Requester aRequester)
{
Error error;
VerifyOrExit(GetState() == kStateStopped,
error = (aServerSockAddr == GetServerAddress()) ? kErrorNone : kErrorBusy);
SuccessOrExit(error = mSocket.Open(Ip6::kNetifThreadInternal));
error = mSocket.Connect(aServerSockAddr);
if (error != kErrorNone)
{
LogInfoOnError(error, "connect to server %s", aServerSockAddr.GetAddress().ToString().AsCString());
IgnoreError(mSocket.Close());
ExitNow();
}
LogInfo("%starting, server %s", (aRequester == kRequesterUser) ? "S" : "Auto-s",
aServerSockAddr.ToString().AsCString());
Resume();
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
if (aRequester == kRequesterAuto)
{
#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE && OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE
Get<Dns::Client>().UpdateDefaultConfigAddress();
#endif
mAutoStart.InvokeCallback(&aServerSockAddr);
}
#endif
exit:
return error;
}
void Client::Stop(Requester aRequester, StopMode aMode)
{
// Change the state of host info and services so that they are
// added/removed again once the client is started back. In the
// case of `kAdding`, we intentionally move to `kToRefresh`
// instead of `kToAdd` since the server may receive our add
// request and the item may be registered on the server. This
// ensures that if we are later asked to remove the item, we do
// notify server.
static const ItemState kNewStateOnStop[]{
/* (0) kToAdd -> */ kToAdd,
/* (1) kAdding -> */ kToRefresh,
/* (2) kToRefresh -> */ kToRefresh,
/* (3) kRefreshing -> */ kToRefresh,
/* (4) kToRemove -> */ kToRemove,
/* (5) kRemoving -> */ kToRemove,
/* (6) kRegistered -> */ kToRefresh,
/* (7) kRemoved -> */ kRemoved,
};
VerifyOrExit(GetState() != kStateStopped);
// State changes:
// kAdding -> kToRefresh
// kRefreshing -> kToRefresh
// kRemoving -> kToRemove
// kRegistered -> kToRefresh
ChangeHostAndServiceStates(kNewStateOnStop, kForAllServices);
IgnoreError(mSocket.Close());
mShouldRemoveKeyLease = false;
mTxFailureRetryCount = 0;
if (aMode == kResetRetryInterval)
{
ResetRetryWaitInterval();
}
SetState(kStateStopped);
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
#if OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
mAutoStart.ResetTimeoutFailureCount();
#endif
if (aRequester == kRequesterAuto)
{
mAutoStart.InvokeCallback(nullptr);
}
#endif
exit:
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
if (aRequester == kRequesterUser)
{
DisableAutoStartMode();
}
#endif
}
void Client::Resume(void)
{
SetState(kStateUpdated);
UpdateState();
}
void Client::Pause(void)
{
// Change the state of host info and services that are are being
// added or removed so that they are added/removed again once the
// client is resumed or started back.
static const ItemState kNewStateOnPause[]{
/* (0) kToAdd -> */ kToAdd,
/* (1) kAdding -> */ kToRefresh,
/* (2) kToRefresh -> */ kToRefresh,
/* (3) kRefreshing -> */ kToRefresh,
/* (4) kToRemove -> */ kToRemove,
/* (5) kRemoving -> */ kToRemove,
/* (6) kRegistered -> */ kRegistered,
/* (7) kRemoved -> */ kRemoved,
};
// State changes:
// kAdding -> kToRefresh
// kRefreshing -> kToRefresh
// kRemoving -> kToRemove
ChangeHostAndServiceStates(kNewStateOnPause, kForAllServices);
SetState(kStatePaused);
}
void Client::HandleNotifierEvents(Events aEvents)
{
if (aEvents.Contains(kEventThreadRoleChanged))
{
HandleRoleChanged();
}
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
if (aEvents.ContainsAny(kEventThreadNetdataChanged | kEventThreadMeshLocalAddrChanged))
{
ProcessAutoStart();
}
#endif
if (aEvents.ContainsAny(kEventIp6AddressAdded | kEventIp6AddressRemoved | kEventThreadMeshLocalAddrChanged) &&
ShouldUpdateHostAutoAddresses())
{
IgnoreError(UpdateHostInfoStateOnAddressChange());
UpdateState();
}
}
void Client::HandleRoleChanged(void)
{
if (Get<Mle::Mle>().IsAttached())
{
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
ApplyAutoStartGuardOnAttach();
#endif
VerifyOrExit(GetState() == kStatePaused);
Resume();
}
else
{
VerifyOrExit(GetState() != kStateStopped);
Pause();
}
exit:
return;
}
#if OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE
Error Client::SetDomainName(const char *aName)
{
Error error = kErrorNone;
VerifyOrExit((mHostInfo.GetState() == kToAdd) || (mHostInfo.GetState() == kRemoved), error = kErrorInvalidState);
mDomainName = (aName != nullptr) ? aName : kDefaultDomainName;
LogInfo("Domain name \"%s\"", mDomainName);
exit:
return error;
}
#endif
Error Client::SetHostName(const char *aName)
{
Error error = kErrorNone;
VerifyOrExit(aName != nullptr, error = kErrorInvalidArgs);
VerifyOrExit((mHostInfo.GetState() == kToAdd) || (mHostInfo.GetState() == kRemoved), error = kErrorInvalidState);
LogInfo("Host name \"%s\"", aName);
mHostInfo.SetName(aName);
mHostInfo.SetState(kToAdd);
UpdateState();
exit:
return error;
}
Error Client::EnableAutoHostAddress(void)
{
Error error = kErrorNone;
VerifyOrExit(!mHostInfo.IsAutoAddressEnabled());
SuccessOrExit(error = UpdateHostInfoStateOnAddressChange());
for (Ip6::Netif::UnicastAddress &unicastAddress : Get<ThreadNetif>().GetUnicastAddresses())
{
unicastAddress.mSrpRegistered = false;
}
mAutoHostAddressCount = 0;
mHostInfo.EnableAutoAddress();
UpdateState();
exit:
return error;
}
Error Client::SetHostAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses)
{
Error error = kErrorNone;
VerifyOrExit((aAddresses != nullptr) && (aNumAddresses > 0), error = kErrorInvalidArgs);
SuccessOrExit(error = UpdateHostInfoStateOnAddressChange());
mHostInfo.SetAddresses(aAddresses, aNumAddresses);
UpdateState();
exit:
return error;
}
void Client::HandleUnicastAddressEvent(Ip6::Netif::AddressEvent aEvent, const Ip6::Netif::UnicastAddress &aAddress)
{
// This callback from `Netif` signals an impending addition or
// removal of a unicast address, occurring before `Notifier`
// events. If `AutoAddress` is enabled, we check whether the
// address origin is SLAAC (e.g., an OMR address) and request a
// longer `TxJitter`. This helps randomize the next SRP
// update transmission time when triggered by an OMR prefix
// change.
VerifyOrExit(IsRunning());
VerifyOrExit(mHostInfo.IsAutoAddressEnabled());
VerifyOrExit(aAddress.GetOrigin() == Ip6::Netif::kOriginSlaac);
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
// The `mGuardTimer`, started by `ApplyAutoStartGuardOnAttach()`,
// tracks a guard interval after the attach event. If an
// address change occurs within this short window, we do not
// apply a longer TX jitter, as this likely indicates a device
// reboot.
VerifyOrExit(!mGuardTimer.IsRunning());
#endif
mTxJitter.Request((aEvent == Ip6::Netif::kAddressAdded) ? TxJitter::kOnSlaacAddrAdd : TxJitter::kOnSlaacAddrRemove);
exit:
return;
}
bool Client::ShouldUpdateHostAutoAddresses(void) const
{
// Determine if any changes to the addresses on `ThreadNetif`
// require registration with the server when `AutoHostAddress` is
// enabled. This includes registering all preferred addresses,
// excluding link-local and mesh-local addresses. If no eligible
// address is available, the ML-EID will be registered.
bool shouldUpdate = false;
uint16_t registeredCount = 0;
Ip6::Netif::UnicastAddress &mlEid = Get<Mle::Mle>().GetMeshLocalEidUnicastAddress();
VerifyOrExit(mHostInfo.IsAutoAddressEnabled());
// We iterate through all eligible addresses on the `ThreadNetif`.
// If we encounter a new address that should be registered but
// isn't, or a previously registered address has been removed, we
// trigger an SRP update to reflect these changes. However, if a
// previously registered address is being deprecated (e.g., due
// to an OMR prefix removal from Network Data), we defer the SRP
// update. The client will re-register after the deprecation
// time has elapsed and the address is removed. In the meantime,
// if any other event triggers the client to send an SRP update,
// the updated address list will be included in that update.
for (const Ip6::Netif::UnicastAddress &unicastAddress : Get<ThreadNetif>().GetUnicastAddresses())
{
if (&unicastAddress == &mlEid)
{
continue;
}
if (ShouldHostAutoAddressRegister(unicastAddress) != unicastAddress.mSrpRegistered)
{
// If this address was previously registered but is no
// longer eligible, we skip sending an immediate update
// only if the address is currently being deprecated
// (it's still valid but no longer preferred).
bool skip = unicastAddress.mSrpRegistered && unicastAddress.mValid && !unicastAddress.mPreferred;
if (!skip)
{
ExitNow(shouldUpdate = true);
}
}
if (unicastAddress.mSrpRegistered)
{
registeredCount++;
}
}
if (registeredCount == 0)
{
ExitNow(shouldUpdate = !mlEid.mSrpRegistered);
}
// Compare the current number of addresses that are marked as
// registered with the previous value `mAutoHostAddressCount`.
// This check handles the case where a previously registered address
// has been removed.
shouldUpdate = (registeredCount != mAutoHostAddressCount);
exit:
return shouldUpdate;
}
bool Client::ShouldHostAutoAddressRegister(const Ip6::Netif::UnicastAddress &aUnicastAddress) const
{
bool shouldRegister = false;
VerifyOrExit(aUnicastAddress.mValid);
VerifyOrExit(aUnicastAddress.mPreferred);
VerifyOrExit(!aUnicastAddress.GetAddress().IsLinkLocalUnicast());
VerifyOrExit(!Get<Mle::Mle>().IsMeshLocalAddress(aUnicastAddress.GetAddress()));
shouldRegister = true;
exit:
return shouldRegister;
}
Error Client::UpdateHostInfoStateOnAddressChange(void)
{
Error error = kErrorNone;
VerifyOrExit((mHostInfo.GetState() != kToRemove) && (mHostInfo.GetState() != kRemoving),
error = kErrorInvalidState);
if (mHostInfo.GetState() == kRemoved)
{
mHostInfo.SetState(kToAdd);
}
else if (mHostInfo.GetState() != kToAdd)
{
mHostInfo.SetState(kToRefresh);
}
exit:
return error;
}
Error Client::AddService(Service &aService)
{
Error error;
VerifyOrExit(mServices.FindMatching(aService) == nullptr, error = kErrorAlready);
SuccessOrExit(error = aService.Init());
mServices.Push(aService);
aService.SetState(kToAdd);
UpdateState();
exit:
return error;
}
Error Client::RemoveService(Service &aService)
{
Error error = kErrorNone;
LinkedList<Service> removedServices;
VerifyOrExit(mServices.Contains(aService), error = kErrorNotFound);
UpdateServiceStateToRemove(aService);
UpdateState();
exit:
return error;
}
void Client::UpdateServiceStateToRemove(Service &aService)
{
if (aService.GetState() != kRemoving)
{
aService.SetState(kToRemove);
}
}
Error Client::ClearService(Service &aService)
{
Error error;
SuccessOrExit(error = mServices.Remove(aService));
aService.SetNext(nullptr);
aService.SetState(kRemoved);
UpdateState();
exit:
return error;
}
Error Client::RemoveHostAndServices(bool aShouldRemoveKeyLease, bool aSendUnregToServer)
{
Error error = kErrorNone;
LogInfo("Remove host & services");
VerifyOrExit(mHostInfo.GetState() != kRemoved, error = kErrorAlready);
if ((mHostInfo.GetState() == kToRemove) || (mHostInfo.GetState() == kRemoving))
{
// Host info remove is already ongoing, if "key lease" remove mode is
// the same, there is no need to send a new update message.
VerifyOrExit(mShouldRemoveKeyLease != aShouldRemoveKeyLease);
}
mShouldRemoveKeyLease = aShouldRemoveKeyLease;
for (Service &service : mServices)
{
UpdateServiceStateToRemove(service);
}
if ((mHostInfo.GetState() == kToAdd) && !aSendUnregToServer)
{
// Host info is not added yet (not yet registered with
// server), so we can remove it and all services immediately.
mHostInfo.SetState(kRemoved);
HandleUpdateDone();
ExitNow();
}
mHostInfo.SetState(kToRemove);
UpdateState();
exit:
return error;
}
void Client::ClearHostAndServices(void)
{
LogInfo("Clear host & services");
switch (GetState())
{
case kStateStopped:
case kStatePaused:
break;
case kStateToUpdate:
case kStateUpdating:
case kStateUpdated:
case kStateToRetry:
SetState(kStateUpdated);
break;
}
mTxFailureRetryCount = 0;
ResetRetryWaitInterval();
mServices.Clear();
mHostInfo.Clear();
}
void Client::SetState(State aState)
{
VerifyOrExit(aState != mState);
LogInfo("State %s -> %s", StateToString(mState), StateToString(aState));
mState = aState;
switch (mState)
{
case kStateStopped:
case kStatePaused:
case kStateUpdated:
mTimer.Stop();
break;
case kStateToUpdate:
mTimer.Start(mTxJitter.DetermineDelay());
break;
case kStateUpdating:
mTimer.Start(GetRetryWaitInterval());
break;
case kStateToRetry:
break;
}
exit:
return;
}
bool Client::ChangeHostAndServiceStates(const ItemState *aNewStates, ServiceStateChangeMode aMode)
{
bool anyChanged;
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
ItemState oldHostState = mHostInfo.GetState();
#endif
anyChanged = mHostInfo.SetState(aNewStates[mHostInfo.GetState()]);
for (Service &service : mServices)
{
if ((aMode == kForServicesAppendedInMessage) && !service.IsAppendedInMessage())
{
continue;
}
anyChanged |= service.SetState(aNewStates[service.GetState()]);
}
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
if ((oldHostState != kRegistered) && (mHostInfo.GetState() == kRegistered))
{
Settings::SrpClientInfo info;
switch (mAutoStart.GetState())
{
case AutoStart::kDisabled:
case AutoStart::kFirstTimeSelecting:
case AutoStart::kReselecting:
break;
case AutoStart::kSelectedUnicastPreferred:
case AutoStart::kSelectedUnicast:
info.SetServerAddress(GetServerAddress().GetAddress());
info.SetServerPort(GetServerAddress().GetPort());
Get<Settings>().Save(info);
break;
case AutoStart::kSelectedAnycast:
Get<Settings>().Delete<Settings::SrpClientInfo>();
break;
}
}
#endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
return anyChanged;
}
void Client::InvokeCallback(Error aError) const { InvokeCallback(aError, mHostInfo, nullptr); }
void Client::InvokeCallback(Error aError, const HostInfo &aHostInfo, const Service *aRemovedServices) const
{
mCallback.InvokeIfSet(aError, &aHostInfo, mServices.GetHead(), aRemovedServices);
}
void Client::SendUpdate(void)
{
static const ItemState kNewStateOnMessageTx[]{
/* (0) kToAdd -> */ kAdding,
/* (1) kAdding -> */ kAdding,
/* (2) kToRefresh -> */ kRefreshing,
/* (3) kRefreshing -> */ kRefreshing,
/* (4) kToRemove -> */ kRemoving,
/* (5) kRemoving -> */ kRemoving,
/* (6) kRegistered -> */ kRegistered,
/* (7) kRemoved -> */ kRemoved,
};
static const ItemState kNewStateOnSingleServiceMode[]{
/* (0) kToAdd -> */ kToAdd,
/* (1) kAdding -> */ kToRefresh,
/* (2) kToRefresh -> */ kToRefresh,
/* (3) kRefreshing -> */ kToRefresh,
/* (4) kToRemove -> */ kToRemove,
/* (5) kRemoving -> */ kToRemove,
/* (6) kRegistered -> */ kRegistered,
/* (7) kRemoved -> */ kRemoved,
};
Error error = kErrorNone;
MsgInfo info;
uint32_t length;
bool anyChanged;
info.mMessage.Reset(mSocket.NewMessage());
VerifyOrExit(info.mMessage != nullptr, error = kErrorNoBufs);
info.mSingleServiceMode = false;
SuccessOrExit(error = PrepareUpdateMessage(info));
length = info.mMessage->GetLength() + sizeof(Ip6::Udp::Header) + sizeof(Ip6::Header);
if (length >= Ip6::kMaxDatagramLength)
{
LogInfo("Msg len %lu is larger than MTU, enabling single service mode", ToUlong(length));
info.mSingleServiceMode = true;
IgnoreError(info.mMessage->SetLength(0));
// State changes:
// kAdding -> kToRefresh
// kRefreshing -> kToRefresh
// kRemoving -> kToRemove
ChangeHostAndServiceStates(kNewStateOnSingleServiceMode, kForServicesAppendedInMessage);
SuccessOrExit(error = PrepareUpdateMessage(info));
}
// State changes:
// kToAdd -> kAdding
// kToRefresh -> kRefreshing
// kToRemove -> kRemoving
anyChanged = ChangeHostAndServiceStates(kNewStateOnMessageTx, kForServicesAppendedInMessage);
if (anyChanged)
{
SelectNewMessageId();
}
SuccessOrExit(error = UpdateIdAndSignatureInUpdateMessage(info));
SuccessOrExit(error = mSocket.SendTo(*info.mMessage, Ip6::MessageInfo()));
// Ownership of the message is transferred to the socket upon a
// successful `SendTo()` call.
info.mMessage.Release();
LogInfo("Send update, msg-id:0x%x", mCurMessageId);
// Remember the update message tx time to use later to determine the
// lease renew time.
mLeaseRenewTime = TimerMilli::GetNow();
mTxFailureRetryCount = 0;
SetState(kStateUpdating);
if (!Get<Mle::Mle>().IsRxOnWhenIdle())
{
// If device is sleepy send fast polls while waiting for
// the response from server.
Get<DataPollSender>().SendFastPolls(kFastPollsAfterUpdateTx);
}
exit:
if (error != kErrorNone)
{
// If there is an error in preparation or transmission of the
// update message (e.g., no buffer to allocate message), up to
// `kMaxTxFailureRetries` times, we wait for a short interval
// `kTxFailureRetryInterval` and try again. After this, we
// continue to retry using the `mRetryWaitInterval` (which keeps
// growing on each failure).
LogInfoOnError(error, "send update");
SetState(kStateToRetry);
if (mTxFailureRetryCount < kMaxTxFailureRetries)
{
uint32_t interval;
mTxFailureRetryCount++;
interval = Random::NonCrypto::AddJitter(kTxFailureRetryInterval, kTxFailureRetryJitter);
mTimer.Start(interval);
LogInfo("Quick retry %u in %lu msec", mTxFailureRetryCount, ToUlong(interval));
// Do not report message preparation errors to user
// until `kMaxTxFailureRetries` are exhausted.
}
else
{
uint16_t retryJitter;
LogRetryWaitInterval();
// Use a divisor of current retry interval for jitter
retryJitter = ClampToUint16(GetRetryWaitInterval() / kRetryJitterDivisor);
retryJitter = Max(retryJitter, kRetryIntervalJitter);
mTimer.Start(Random::NonCrypto::AddJitter(GetRetryWaitInterval(), retryJitter));
GrowRetryWaitInterval();
InvokeCallback(error);
}
}
}
Error Client::PrepareUpdateMessage(MsgInfo &aInfo)
{
constexpr uint16_t kHeaderOffset = 0;
Error error = kErrorNone;
Dns::UpdateHeader header;
aInfo.mDomainNameOffset = MsgInfo::kUnknownOffset;
aInfo.mHostNameOffset = MsgInfo::kUnknownOffset;
aInfo.mRecordCount = 0;
#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
aInfo.mKeyInfo.SetKeyRef(Get<Crypto::Storage::KeyRefManager>().KeyRefFor(Crypto::Storage::KeyRefManager::kEcdsa));
#endif
SuccessOrExit(error = ReadOrGenerateKey(aInfo.mKeyInfo));
header.SetMessageId(mCurMessageId);
// SRP Update (DNS Update) message must have exactly one record in
// Zone section, no records in Prerequisite Section, can have
// multiple records in Update Section (tracked as they are added),
// and two records in Additional Data Section (OPT and SIG records).
// The SIG record itself should not be included in calculation of
// SIG(0) signature, so the addition record count is set to one
// here. After signature calculation and appending of SIG record,
// the additional record count is updated to two and the header is
// rewritten in the message.
header.SetZoneRecordCount(1);
header.SetAdditionalRecordCount(1);
SuccessOrExit(error = aInfo.mMessage->Append(header));
// Prepare Zone section
aInfo.mDomainNameOffset = aInfo.mMessage->GetLength();
SuccessOrExit(error = Dns::Name::AppendName(mDomainName, *aInfo.mMessage));
SuccessOrExit(error = aInfo.mMessage->Append(Dns::Zone()));
// Prepare Update section
SuccessOrExit(error = AppendServiceInstructions(aInfo));
SuccessOrExit(error = AppendHostDescriptionInstruction(aInfo));
header.SetUpdateRecordCount(aInfo.mRecordCount);
aInfo.mMessage->Write(kHeaderOffset, header);
// Prepare Additional Data section
SuccessOrExit(error = AppendUpdateLeaseOptRecord(aInfo));
SuccessOrExit(error = AppendSignature(aInfo, kAppendEmptySignature));
exit:
return error;
}
Error Client::UpdateIdAndSignatureInUpdateMessage(MsgInfo &aInfo)
{
constexpr uint16_t kHeaderOffset = 0;
Error error;
Dns::UpdateHeader header;
IgnoreError(aInfo.mMessage->Read(kHeaderOffset, header));
header.SetMessageId(mCurMessageId);
aInfo.mMessage->Write(kHeaderOffset, header);
SuccessOrExit(error = AppendSignature(aInfo, kOverwriteWithNewSignature));
header.SetAdditionalRecordCount(2); // Lease OPT and SIG RRs
aInfo.mMessage->Write(kHeaderOffset, header);
exit:
return error;
}
#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
Error Client::ReadOrGenerateKey(KeyInfo &aKeyInfo)
{
Error error = kErrorNone;
Crypto::Ecdsa::P256::KeyPair keyPair;
VerifyOrExit(!Crypto::Storage::HasKey(aKeyInfo.GetKeyRef()));
error = Get<Settings>().Read<Settings::SrpEcdsaKey>(keyPair);
if (error == kErrorNone)
{
if (aKeyInfo.ImportKeyPair(keyPair) != kErrorNone)
{
SuccessOrExit(error = aKeyInfo.Generate());
}
Get<Settings>().Delete<Settings::SrpEcdsaKey>();
}
else
{
SuccessOrExit(error = aKeyInfo.Generate());
}
exit:
return error;
}
#else
Error Client::ReadOrGenerateKey(KeyInfo &aKeyInfo)
{
Error error;
error = Get<Settings>().Read<Settings::SrpEcdsaKey>(aKeyInfo);
if (error == kErrorNone)
{
Crypto::Ecdsa::P256::PublicKey publicKey;
if (aKeyInfo.GetPublicKey(publicKey) == kErrorNone)
{
ExitNow();
}
}
SuccessOrExit(error = aKeyInfo.Generate());
Get<Settings>().Save<Settings::SrpEcdsaKey>(aKeyInfo);
exit:
return error;
}
#endif // OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
Error Client::AppendServiceInstructions(MsgInfo &aInfo)
{
Error error = kErrorNone;
if ((mHostInfo.GetState() == kToRemove) || (mHostInfo.GetState() == kRemoving))
{
// When host is being removed, there is no need to include
// services in the message (server is expected to remove any
// previously registered services by this client). However, we
// still mark all services as if they are appended in the message
// so to ensure to update their state after sending the message.
for (Service &service : mServices)
{
service.MarkAsAppendedInMessage();
}
mLease = 0;
mKeyLease = mShouldRemoveKeyLease ? 0 : mDefaultKeyLease;
ExitNow();
}
mLease = kUnspecifiedInterval;
mKeyLease = kUnspecifiedInterval;
// We first go through all services which are being updated (in any
// of `...ing` states) and determine the lease and key lease intervals
// associated with them. By the end of the loop either of `mLease` or
// `mKeyLease` may be set or may still remain `kUnspecifiedInterval`.
for (Service &service : mServices)
{
uint32_t lease = DetermineLeaseInterval(service.GetLease(), mDefaultLease);
uint32_t keyLease = Max(DetermineLeaseInterval(service.GetKeyLease(), mDefaultKeyLease), lease);
service.ClearAppendedInMessageFlag();
switch (service.GetState())
{
case kAdding:
case kRefreshing:
OT_ASSERT((mLease == kUnspecifiedInterval) || (mLease == lease));
mLease = lease;
OT_FALL_THROUGH;
case kRemoving:
OT_ASSERT((mKeyLease == kUnspecifiedInterval) || (mKeyLease == keyLease));
mKeyLease = keyLease;
break;
case kToAdd:
case kToRefresh:
case kToRemove:
case kRegistered:
case kRemoved:
break;
}
}
// We go through all services again and append the services that
// match the selected `mLease` and `mKeyLease`. If the lease intervals
// are not yet set, the first appended service will determine them.
for (Service &service : mServices)
{
// Skip over services that are already registered in this loop.
// They may be added from the loop below once the lease intervals
// are determined.
if ((service.GetState() != kRegistered) && CanAppendService(service))
{
SuccessOrExit(error = AppendServiceInstruction(service, aInfo));
if (aInfo.mSingleServiceMode)
{
// In "single service mode", we allow only one service
// to be appended in the message.
break;
}
}
}
if (!aInfo.mSingleServiceMode)
{
for (Service &service : mServices)
{
if ((service.GetState() == kRegistered) && CanAppendService(service) && ShouldRenewEarly(service))
{
// If the lease needs to be renewed or if we are close to the
// renewal time of a registered service, we refresh the service
// early and include it in this update. This helps put more
// services on the same lease refresh schedule.
service.SetState(kToRefresh);
SuccessOrExit(error = AppendServiceInstruction(service, aInfo));
}
}
}
// `mLease` or `mKeylease` may be determined from the set of
// services included in the message. If they are not yet set we
// use the default intervals.
mLease = DetermineLeaseInterval(mLease, mDefaultLease);
mKeyLease = DetermineLeaseInterval(mKeyLease, mDefaultKeyLease);
// When message only contains removal of a previously registered
// service, then `mKeyLease` is set but `mLease` remains unspecified.
// In such a case, we end up using `mDefaultLease` but then we need
// to make sure it is not greater than the selected `mKeyLease`.
mLease = Min(mLease, mKeyLease);
exit:
return error;
}
bool Client::CanAppendService(const Service &aService)
{
// Check the lease intervals associated with `aService` to see if
// it can be included in this message. When removing a service,
// only key lease interval should match. In all other cases, both
// lease and key lease should match. The `mLease` and/or `mKeyLease`
// may be updated if they were unspecified.
bool canAppend = false;
uint32_t lease = DetermineLeaseInterval(aService.GetLease(), mDefaultLease);
uint32_t keyLease = Max(DetermineLeaseInterval(aService.GetKeyLease(), mDefaultKeyLease), lease);
switch (aService.GetState())
{
case kToAdd:
case kAdding:
case kToRefresh:
case kRefreshing:
case kRegistered:
VerifyOrExit((mLease == kUnspecifiedInterval) || (mLease == lease));
VerifyOrExit((mKeyLease == kUnspecifiedInterval) || (mKeyLease == keyLease));
mLease = lease;
mKeyLease = keyLease;
canAppend = true;
break;
case kToRemove:
case kRemoving:
VerifyOrExit((mKeyLease == kUnspecifiedInterval) || (mKeyLease == keyLease));
mKeyLease = keyLease;
canAppend = true;
break;
case kRemoved:
break;
}
exit:
return canAppend;
}
Error Client::AppendServiceInstruction(Service &aService, MsgInfo &aInfo)
{
Error error = kErrorNone;
bool removing = ((aService.GetState() == kToRemove) || (aService.GetState() == kRemoving));
Dns::ResourceRecord rr;
Dns::SrvRecord srv;
uint16_t serviceNameOffset;
uint16_t instanceNameOffset;
uint16_t offset;
aService.MarkAsAppendedInMessage();
//----------------------------------
// Service Discovery Instruction
// PTR record
// "service name labels" + (pointer to) domain name.
serviceNameOffset = aInfo.mMessage->GetLength();
SuccessOrExit(error = Dns::Name::AppendMultipleLabels(aService.GetName(), *aInfo.mMessage));
SuccessOrExit(error = Dns::Name::AppendPointerLabel(aInfo.mDomainNameOffset, *aInfo.mMessage));
// On remove, we use "Delete an RR from an RRSet" where class is set
// to NONE and TTL to zero (RFC 2136 - section 2.5.4).
rr.Init(Dns::ResourceRecord::kTypePtr, removing ? Dns::PtrRecord::kClassNone : Dns::PtrRecord::kClassInternet);
rr.SetTtl(removing ? 0 : DetermineTtl());
offset = aInfo.mMessage->GetLength();
SuccessOrExit(error = aInfo.mMessage->Append(rr));
// "Instance name" + (pointer to) service name.
instanceNameOffset = aInfo.mMessage->GetLength();
SuccessOrExit(error = Dns::Name::AppendLabel(aService.GetInstanceName(), *aInfo.mMessage));
SuccessOrExit(error = Dns::Name::AppendPointerLabel(serviceNameOffset, *aInfo.mMessage));
Dns::ResourceRecord::UpdateRecordLengthInMessage(*aInfo.mMessage, offset);
aInfo.mRecordCount++;
if (aService.HasSubType() && !removing)
{
const char *subTypeLabel;
uint16_t subServiceNameOffset = 0;
for (uint16_t index = 0; (subTypeLabel = aService.GetSubTypeLabelAt(index)) != nullptr; ++index)
{
// subtype label + "_sub" label + (pointer to) service name.
SuccessOrExit(error = Dns::Name::AppendLabel(subTypeLabel, *aInfo.mMessage));
if (index == 0)
{
subServiceNameOffset = aInfo.mMessage->GetLength();
SuccessOrExit(error = Dns::Name::AppendLabel("_sub", *aInfo.mMessage));
SuccessOrExit(error = Dns::Name::AppendPointerLabel(serviceNameOffset, *aInfo.mMessage));
}
else
{
SuccessOrExit(error = Dns::Name::AppendPointerLabel(subServiceNameOffset, *aInfo.mMessage));
}
// `rr` is already initialized as PTR.
offset = aInfo.mMessage->GetLength();
SuccessOrExit(error = aInfo.mMessage->Append(rr));
SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, *aInfo.mMessage));
Dns::ResourceRecord::UpdateRecordLengthInMessage(*aInfo.mMessage, offset);
aInfo.mRecordCount++;
}
}
//----------------------------------
// Service Description Instruction
// "Delete all RRsets from a name" for Instance Name.
SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, *aInfo.mMessage));
SuccessOrExit(error = AppendDeleteAllRrsets(aInfo));
aInfo.mRecordCount++;
VerifyOrExit(!removing);
// SRV RR
SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, *aInfo.mMessage));
srv.Init();
srv.SetTtl(DetermineTtl());
srv.SetPriority(aService.GetPriority());
srv.SetWeight(aService.GetWeight());
srv.SetPort(aService.GetPort());
offset = aInfo.mMessage->GetLength();
SuccessOrExit(error = aInfo.mMessage->Append(srv));
SuccessOrExit(error = AppendHostName(aInfo));
Dns::ResourceRecord::UpdateRecordLengthInMessage(*aInfo.mMessage, offset);
aInfo.mRecordCount++;
// TXT RR
SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, *aInfo.mMessage));
rr.Init(Dns::ResourceRecord::kTypeTxt);
offset = aInfo.mMessage->GetLength();
SuccessOrExit(error = aInfo.mMessage->Append(rr));
SuccessOrExit(
error = Dns::TxtEntry::AppendEntries(aService.GetTxtEntries(), aService.GetNumTxtEntries(), *aInfo.mMessage));
Dns::ResourceRecord::UpdateRecordLengthInMessage(*aInfo.mMessage, offset);
aInfo.mRecordCount++;
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
if (mServiceKeyRecordEnabled)
{
// KEY RR is optional in "Service Description Instruction". It
// is added here under `REFERENCE_DEVICE` config and is intended
// for testing only.
SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, *aInfo.mMessage));
SuccessOrExit(error = AppendKeyRecord(aInfo));
}
#endif
exit:
return error;
}
Error Client::AppendHostDescriptionInstruction(MsgInfo &aInfo)
{
Error error = kErrorNone;
//----------------------------------
// Host Description Instruction
// "Delete all RRsets from a name" for Host Name.
SuccessOrExit(error = AppendHostName(aInfo));
SuccessOrExit(error = AppendDeleteAllRrsets(aInfo));
aInfo.mRecordCount++;
// AAAA RRs
if (mHostInfo.IsAutoAddressEnabled())
{
// Append all preferred addresses on Thread netif excluding link-local
// and mesh-local addresses. If no address is appended, we include
// the mesh local EID.
mAutoHostAddressCount = 0;
for (Ip6::Netif::UnicastAddress &unicastAddress : Get<ThreadNetif>().GetUnicastAddresses())
{
if (ShouldHostAutoAddressRegister(unicastAddress))
{
SuccessOrExit(error = AppendAaaaRecord(unicastAddress.GetAddress(), aInfo));
unicastAddress.mSrpRegistered = true;
mAutoHostAddressCount++;
}
else
{
unicastAddress.mSrpRegistered = false;
}
}
if (mAutoHostAddressCount == 0)
{
Ip6::Netif::UnicastAddress &mlEid = Get<Mle::Mle>().GetMeshLocalEidUnicastAddress();
SuccessOrExit(error = AppendAaaaRecord(mlEid.GetAddress(), aInfo));
mlEid.mSrpRegistered = true;
mAutoHostAddressCount++;
}
}
else
{
for (uint8_t index = 0; index < mHostInfo.GetNumAddresses(); index++)
{
SuccessOrExit(error = AppendAaaaRecord(mHostInfo.GetAddress(index), aInfo));
}
}
// KEY RR
SuccessOrExit(error = AppendHostName(aInfo));
SuccessOrExit(error = AppendKeyRecord(aInfo));
exit:
return error;
}
Error Client::AppendAaaaRecord(const Ip6::Address &aAddress, MsgInfo &aInfo) const
{
Error error;
Dns::ResourceRecord rr;
rr.Init(Dns::ResourceRecord::kTypeAaaa);
rr.SetTtl(DetermineTtl());
rr.SetLength(sizeof(Ip6::Address));
SuccessOrExit(error = AppendHostName(aInfo));
SuccessOrExit(error = aInfo.mMessage->Append(rr));
SuccessOrExit(error = aInfo.mMessage->Append(aAddress));
aInfo.mRecordCount++;
exit:
return error;
}
Error Client::AppendKeyRecord(MsgInfo &aInfo) const
{
Error error;
Dns::KeyRecord key;
Crypto::Ecdsa::P256::PublicKey publicKey;
key.Init();
key.SetTtl(DetermineTtl());
key.SetFlags(Dns::KeyRecord::kAuthConfidPermitted, Dns::KeyRecord::kOwnerNonZone,
Dns::KeyRecord::kSignatoryFlagGeneral);
key.SetProtocol(Dns::KeyRecord::kProtocolDnsSec);
key.SetAlgorithm(Dns::KeyRecord::kAlgorithmEcdsaP256Sha256);
key.SetLength(sizeof(Dns::KeyRecord) - sizeof(Dns::ResourceRecord) + sizeof(Crypto::Ecdsa::P256::PublicKey));
SuccessOrExit(error = aInfo.mMessage->Append(key));
SuccessOrExit(error = aInfo.mKeyInfo.GetPublicKey(publicKey));
SuccessOrExit(error = aInfo.mMessage->Append(publicKey));
aInfo.mRecordCount++;
exit:
return error;
}
Error Client::AppendDeleteAllRrsets(MsgInfo &aInfo) const
{
// "Delete all RRsets from a name" (RFC 2136 - 2.5.3)
// Name should be already appended in the message.
Dns::ResourceRecord rr;
rr.Init(Dns::ResourceRecord::kTypeAny, Dns::ResourceRecord::kClassAny);
rr.SetTtl(0);
rr.SetLength(0);
return aInfo.mMessage->Append(rr);
}
Error Client::AppendHostName(MsgInfo &aInfo, bool aDoNotCompress) const
{
Error error;
if (aDoNotCompress)
{
// Uncompressed (canonical form) of host name is used for SIG(0)
// calculation.
SuccessOrExit(error = Dns::Name::AppendMultipleLabels(mHostInfo.GetName(), *aInfo.mMessage));
error = Dns::Name::AppendName(mDomainName, *aInfo.mMessage);
ExitNow();
}
// If host name was previously added in the message, add it
// compressed as pointer to the previous one. Otherwise,
// append it and remember the offset.
if (aInfo.mHostNameOffset != MsgInfo::kUnknownOffset)
{
ExitNow(error = Dns::Name::AppendPointerLabel(aInfo.mHostNameOffset, *aInfo.mMessage));
}
aInfo.mHostNameOffset = aInfo.mMessage->GetLength();
SuccessOrExit(error = Dns::Name::AppendMultipleLabels(mHostInfo.GetName(), *aInfo.mMessage));
error = Dns::Name::AppendPointerLabel(aInfo.mDomainNameOffset, *aInfo.mMessage);
exit:
return error;
}
Error Client::AppendUpdateLeaseOptRecord(MsgInfo &aInfo)
{
Error error;
Dns::OptRecord optRecord;
Dns::LeaseOption leaseOption;
uint16_t optionSize;
// Append empty (root domain) as OPT RR name.
SuccessOrExit(error = Dns::Name::AppendTerminator(*aInfo.mMessage));
// `Init()` sets the type and clears (set to zero) the extended
// Response Code, version and all flags.
optRecord.Init();
optRecord.SetUdpPayloadSize(kUdpPayloadSize);
optRecord.SetDnsSecurityFlag();
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
if (mUseShortLeaseOption)
{
LogInfo("Test mode - appending short variant of Lease Option");
mKeyLease = mLease;
leaseOption.InitAsShortVariant(mLease);
}
else
#endif
{
leaseOption.InitAsLongVariant(mLease, mKeyLease);
}
optionSize = static_cast<uint16_t>(leaseOption.GetSize());
optRecord.SetLength(optionSize);
SuccessOrExit(error = aInfo.mMessage->Append(optRecord));
error = aInfo.mMessage->AppendBytes(&leaseOption, optionSize);
exit:
return error;
}
Error Client::AppendSignature(MsgInfo &aInfo, SignatureAppendMode aMode)
{
Error error;
Dns::SigRecord sig;
Crypto::Sha256 sha256;
Crypto::Sha256::Hash hash;
Crypto::Ecdsa::P256::Signature signature;
uint16_t offset;
uint16_t len;
// Prepare SIG RR: TTL, type covered, labels count should be set
// to zero. Since we have no clock, inception and expiration time
// are also set to zero. The RDATA length will be set later (not
// yet known due to variably (and possible compression) of signer's
// name.
sig.Clear();
sig.Init(Dns::ResourceRecord::kClassAny);
sig.SetAlgorithm(Dns::KeyRecord::kAlgorithmEcdsaP256Sha256);
switch (aMode)
{
case kAppendEmptySignature:
aInfo.mSigRecordOffset = aInfo.mMessage->GetLength();
break;
case kOverwriteWithNewSignature:
// Revert back to the start of signature record.
IgnoreError(aInfo.mMessage->SetLength(aInfo.mSigRecordOffset));
// Append the SIG RR with full uncompressed form of the host name
// as the signer's name. This is used for SIG(0) calculation only.
// It will be overwritten with host name compressed.
offset = aInfo.mMessage->GetLength();
SuccessOrExit(error = aInfo.mMessage->Append(sig));
SuccessOrExit(error = AppendHostName(aInfo, /* aDoNotCompress */ true));
// Calculate signature (RFC 2931): Calculated over "data" which is
// concatenation of (1) the SIG RR RDATA wire format (including
// the canonical form of the signer's name), entirely omitting the
// signature subfield, (2) DNS query message, including DNS header
// but not UDP/IP header before the header RR counts have been
// adjusted for the inclusion of SIG(0).
sha256.Start();
// (1) SIG RR RDATA wire format
len = aInfo.mMessage->GetLength() - offset - sizeof(Dns::ResourceRecord);
sha256.Update(*aInfo.mMessage, offset + sizeof(Dns::ResourceRecord), len);
// (2) Message from DNS header before SIG
sha256.Update(*aInfo.mMessage, 0, offset);
sha256.Finish(hash);
SuccessOrExit(error = aInfo.mKeyInfo.Sign(hash, signature));
// Move back in message and append SIG RR now with compressed host
// name (as signer's name) along with the calculated signature.
IgnoreError(aInfo.mMessage->SetLength(offset));
break;
}
// SIG(0) uses owner name of root (single zero byte).
SuccessOrExit(error = Dns::Name::AppendTerminator(*aInfo.mMessage));
offset = aInfo.mMessage->GetLength();
SuccessOrExit(error = aInfo.mMessage->Append(sig));
SuccessOrExit(error = AppendHostName(aInfo));
SuccessOrExit(error = aInfo.mMessage->Append(signature));
Dns::ResourceRecord::UpdateRecordLengthInMessage(*aInfo.mMessage, offset);
exit:
return error;
}
void Client::HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
{
OT_UNUSED_VARIABLE(aMessageInfo);
ProcessResponse(aMessage);
}
void Client::ProcessResponse(Message &aMessage)
{
static const ItemState kNewStateOnUpdateDone[]{
/* (0) kToAdd -> */ kToAdd,
/* (1) kAdding -> */ kRegistered,
/* (2) kToRefresh -> */ kToRefresh,
/* (3) kRefreshing -> */ kRegistered,
/* (4) kToRemove -> */ kToRemove,
/* (5) kRemoving -> */ kRemoved,
/* (6) kRegistered -> */ kRegistered,
/* (7) kRemoved -> */ kRemoved,
};
Error error = kErrorNone;
Dns::UpdateHeader header;
uint16_t offset = aMessage.GetOffset();
uint16_t recordCount;
LinkedList<Service> removedServices;
switch (GetState())
{
case kStateToUpdate:
case kStateUpdating:
case kStateToRetry:
break;
case kStateStopped:
case kStatePaused:
case kStateUpdated:
ExitNow();
}
SuccessOrExit(error = aMessage.Read(offset, header));
VerifyOrExit(header.GetType() == Dns::Header::kTypeResponse, error = kErrorParse);
VerifyOrExit(header.GetQueryType() == Dns::Header::kQueryTypeUpdate, error = kErrorParse);
VerifyOrExit(header.GetMessageId() == mCurMessageId, error = kErrorDrop);
if (!Get<Mle::Mle>().IsRxOnWhenIdle())
{
Get<DataPollSender>().StopFastPolls();
}
LogInfo("Received response, msg-id:0x%x", header.GetMessageId());
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
mAutoStart.ResetTimeoutFailureCount();
#endif
error = Dns::Header::ResponseCodeToError(header.GetResponseCode());
if (error != kErrorNone)
{
LogInfo("Server rejected %s code:%d", ErrorToString(error), header.GetResponseCode());
if (mHostInfo.GetState() == kAdding)
{
// Since server rejected the update message, we go back to
// `kToAdd` state to allow user to give a new name using
// `SetHostName()`.
mHostInfo.SetState(kToAdd);
}
// Wait for the timer to expire to retry. Note that timer is
// already scheduled for the current wait interval when state
// was changed to `kStateUpdating`.
LogRetryWaitInterval();
GrowRetryWaitInterval();
SetState(kStateToRetry);
InvokeCallback(error);
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
if ((error == kErrorDuplicated) || (error == kErrorSecurity))
{
// If the server rejects the update with specific errors
// (indicating duplicate name and/or security error), we
// try to switch the server (we check if another can be
// found in the Network Data).
//
// Note that this is done after invoking the callback and
// notifying the user of the error from server. This works
// correctly even if user makes changes from callback
// (e.g., calls SRP client APIs like `Stop` or disables
// auto-start), since we have a guard check at the top of
// `SelectNextServer()` to verify that client is still
// running and auto-start is enabled and selected the
// server.
SelectNextServer(/* aDisallowSwitchOnRegisteredHost */ true);
}
#endif
ExitNow(error = kErrorNone);
}
offset += sizeof(header);
// Skip over all sections till Additional Data section
// SPEC ENHANCEMENT: Server can echo the request back or not
// include any of RRs. Would be good to explicitly require SRP server
// to not echo back RRs.
if (header.GetZoneRecordCount() != 0)
{
VerifyOrExit(header.GetZoneRecordCount() == 1, error = kErrorParse);
SuccessOrExit(error = Dns::Name::ParseName(aMessage, offset));
VerifyOrExit(offset + sizeof(Dns::Zone) <= aMessage.GetLength(), error = kErrorParse);
offset += sizeof(Dns::Zone);
}
// Check for Update Lease OPT RR. This determines the lease
// interval accepted by server. If not present, then use the
// transmitted lease interval from the update request message.
recordCount =
header.GetPrerequisiteRecordCount() + header.GetUpdateRecordCount() + header.GetAdditionalRecordCount();
while (recordCount > 0)
{
uint16_t startOffset = offset;
Dns::ResourceRecord rr;
SuccessOrExit(error = ReadResourceRecord(aMessage, offset, rr));
recordCount--;
if (rr.GetType() == Dns::ResourceRecord::kTypeOpt)
{
SuccessOrExit(error = ProcessOptRecord(aMessage, startOffset, static_cast<Dns::OptRecord &>(rr)));
}
}
// Calculate the lease renew time based on update message tx time
// and the lease time. `kLeaseRenewGuardInterval` is used to
// ensure that we renew the lease before server expires it. In the
// unlikely (but maybe useful for testing) case where the accepted
// lease interval is too short (shorter than twice the guard time)
// we just use half of the accepted lease interval.
if (mLease > 2 * kLeaseRenewGuardInterval)
{
uint32_t interval = Time::SecToMsec(mLease - kLeaseRenewGuardInterval);
mLeaseRenewTime += Random::NonCrypto::AddJitter(interval, kLeaseRenewJitter);
}
else
{
mLeaseRenewTime += Time::SecToMsec(mLease) / 2;
}
for (Service &service : mServices)
{
if ((service.GetState() == kAdding) || (service.GetState() == kRefreshing))
{
service.SetLeaseRenewTime(mLeaseRenewTime);
}
}
// State changes:
// kAdding -> kRegistered
// kRefreshing -> kRegistered
// kRemoving -> kRemoved
ChangeHostAndServiceStates(kNewStateOnUpdateDone, kForServicesAppendedInMessage);
HandleUpdateDone();
UpdateState();
exit:
LogInfoOnError(error, "process response");
}
void Client::SelectNewMessageId(void)
{
uint16_t oldId = mCurMessageId;
do
{
mCurMessageId = Random::NonCrypto::GetUint16();
} while (oldId == mCurMessageId);
}
void Client::HandleUpdateDone(void)
{
HostInfo hostInfoCopy = mHostInfo;
LinkedList<Service> removedServices;
if (mHostInfo.GetState() == kRemoved)
{
mHostInfo.Clear();
}
ResetRetryWaitInterval();
SetState(kStateUpdated);
GetRemovedServices(removedServices);
InvokeCallback(kErrorNone, hostInfoCopy, removedServices.GetHead());
}
void Client::GetRemovedServices(LinkedList<Service> &aRemovedServices)
{
mServices.RemoveAllMatching(aRemovedServices, kRemoved);
}
Error Client::ReadResourceRecord(const Message &aMessage, uint16_t &aOffset, Dns::ResourceRecord &aRecord)
{
// Reads and skips over a Resource Record (RR) from message at
// given offset. On success, `aOffset` is updated to point to end
// of RR.
Error error;
SuccessOrExit(error = Dns::Name::ParseName(aMessage, aOffset));
SuccessOrExit(error = aMessage.Read(aOffset, aRecord));
VerifyOrExit(aOffset + aRecord.GetSize() <= aMessage.GetLength(), error = kErrorParse);
aOffset += static_cast<uint16_t>(aRecord.GetSize());
exit:
return error;
}
Error Client::ProcessOptRecord(const Message &aMessage, uint16_t aOffset, const Dns::OptRecord &aOptRecord)
{
// Read and process all options (in an OPT RR) from a message.
// The `aOffset` points to beginning of record in `aMessage`.
Error error = kErrorNone;
Dns::LeaseOption leaseOption;
IgnoreError(Dns::Name::ParseName(aMessage, aOffset));
aOffset += sizeof(Dns::OptRecord);
switch (error = leaseOption.ReadFrom(aMessage, aOffset, aOptRecord.GetLength()))
{
case kErrorNone:
mLease = Min(leaseOption.GetLeaseInterval(), kMaxLease);
mKeyLease = Min(leaseOption.GetKeyLeaseInterval(), kMaxLease);
break;
case kErrorNotFound:
// If server does not include a lease option in its response, it
// indicates that it accepted what we requested.
error = kErrorNone;
break;
default:
ExitNow();
}
exit:
return error;
}
void Client::UpdateState(void)
{
NextFireTime nextRenewTime;
bool shouldUpdate = false;
VerifyOrExit((GetState() != kStateStopped) && (GetState() != kStatePaused));
VerifyOrExit(mHostInfo.GetName() != nullptr);
// Go through the host info and all the services to check if there
// are any new changes (i.e., anything new to add or remove). This
// is used to determine whether to send an SRP update message or
// not. Also keep track of the earliest renew time among the
// previously registered services. This is used to schedule the
// timer for next refresh.
switch (mHostInfo.GetState())
{
case kAdding:
case kRefreshing:
case kRemoving:
break;
case kRegistered:
if (nextRenewTime.GetNow() < mLeaseRenewTime)
{
break;
}
mHostInfo.SetState(kToRefresh);
OT_FALL_THROUGH;
case kToAdd:
case kToRefresh:
// Make sure we have at least one service and at least one
// host address, otherwise no need to send SRP update message.
// The exception is when removing host info where we allow
// for empty service list.
VerifyOrExit(!mServices.IsEmpty() && (mHostInfo.IsAutoAddressEnabled() || (mHostInfo.GetNumAddresses() > 0)));
OT_FALL_THROUGH;
case kToRemove:
shouldUpdate = true;
break;
case kRemoved:
ExitNow();
}
// If host info is being removed, we skip over checking service list
// for new adds (or removes). This handles the situation where while
// remove is ongoing and before we get a response from the server,
// user adds a new service to be registered. We wait for remove to
// finish (receive response from server) before starting with a new
// service adds.
if (mHostInfo.GetState() != kRemoving)
{
for (Service &service : mServices)
{
switch (service.GetState())
{
case kToAdd:
case kToRefresh:
case kToRemove:
shouldUpdate = true;
break;
case kRegistered:
if (service.GetLeaseRenewTime() <= nextRenewTime.GetNow())
{
service.SetState(kToRefresh);
shouldUpdate = true;
}
else
{
nextRenewTime.UpdateIfEarlier(service.GetLeaseRenewTime());
}
break;
case kAdding:
case kRefreshing:
case kRemoving:
case kRemoved:
break;
}
}
}
if (shouldUpdate)
{
SetState(kStateToUpdate);
ExitNow();
}
if (GetState() == kStateUpdated)
{
mTimer.FireAt(nextRenewTime);
}
exit:
return;
}
void Client::GrowRetryWaitInterval(void)
{
mRetryWaitInterval =
mRetryWaitInterval / kRetryIntervalGrowthFactorDenominator * kRetryIntervalGrowthFactorNumerator;
mRetryWaitInterval = Min(mRetryWaitInterval, kMaxRetryWaitInterval);
}
uint32_t Client::DetermineLeaseInterval(uint32_t aInterval, uint32_t aDefaultInterval) const
{
// Determine the lease or key lease interval.
//
// We use `aInterval` if it is non-zero, otherwise, use the
// `aDefaultInterval`. We also ensure that the returned value is
// never greater than `kMaxLease`. The `kMaxLease` is selected
// such the lease intervals in msec can still fit in a `uint32_t`
// `Time` variable (`kMaxLease` is ~ 24.8 days).
return Min(kMaxLease, (aInterval != kUnspecifiedInterval) ? aInterval : aDefaultInterval);
}
uint32_t Client::DetermineTtl(void) const
{
// Determine the TTL to use based on current `mLease`.
// If `mLease == 0`, it indicates we are removing host
// and so we use `mDefaultLease` instead.
uint32_t lease = (mLease == 0) ? mDefaultLease : mLease;
return (mTtl == kUnspecifiedInterval) ? lease : Min(mTtl, lease);
}
bool Client::ShouldRenewEarly(const Service &aService) const
{
// Check if we reached the service renew time or close to it. The
// "early renew interval" is used to allow early refresh. It is
// calculated as a factor of the service requested lease interval.
// The "early lease renew factor" is given as a fraction (numerator
// and denominator). If the denominator is set to zero (i.e., factor
// is set to infinity), then service is always included in all SRP
// update messages.
bool shouldRenew;
#if OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_DENOMINATOR != 0
uint32_t earlyRenewInterval;
earlyRenewInterval = Time::SecToMsec(DetermineLeaseInterval(aService.GetLease(), mDefaultLease));
earlyRenewInterval = earlyRenewInterval / kEarlyLeaseRenewFactorDenominator * kEarlyLeaseRenewFactorNumerator;
shouldRenew = (aService.GetLeaseRenewTime() <= TimerMilli::GetNow() + earlyRenewInterval);
#else
OT_UNUSED_VARIABLE(aService);
shouldRenew = true;
#endif
return shouldRenew;
}
void Client::HandleTimer(void)
{
switch (GetState())
{
case kStateStopped:
case kStatePaused:
break;
case kStateToUpdate:
case kStateToRetry:
SendUpdate();
break;
case kStateUpdating:
LogRetryWaitInterval();
LogInfo("Timed out, no response");
GrowRetryWaitInterval();
SetState(kStateToUpdate);
InvokeCallback(kErrorResponseTimeout);
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
// After certain number of back-to-back timeout failures, we try
// to switch the server. This is again done after invoking the
// callback. It works correctly due to the guard check at the
// top of `SelectNextServer()`.
mAutoStart.IncrementTimeoutFailureCount();
if (mAutoStart.GetTimeoutFailureCount() >= kMaxTimeoutFailuresToSwitchServer)
{
SelectNextServer(kDisallowSwitchOnRegisteredHost);
}
#endif
break;
case kStateUpdated:
UpdateState();
break;
}
}
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
void Client::EnableAutoStartMode(AutoStartCallback aCallback, void *aContext)
{
mAutoStart.SetCallback(aCallback, aContext);
VerifyOrExit(mAutoStart.GetState() == AutoStart::kDisabled);
mAutoStart.SetState(AutoStart::kFirstTimeSelecting);
ApplyAutoStartGuardOnAttach();
ProcessAutoStart();
exit:
return;
}
void Client::ApplyAutoStartGuardOnAttach(void)
{
VerifyOrExit(Get<Mle::Mle>().IsAttached());
VerifyOrExit(!IsRunning());
VerifyOrExit(mAutoStart.GetState() == AutoStart::kFirstTimeSelecting);
// The `mGuardTimer` tracks a guard interval after the attach
// event while `AutoStart` has yet to select a server for the
// first time.
//
// This is used by `ProcessAutoStart()` to apply different TX
// jitter values. If server selection occurs within this short
// window, a shorter TX jitter is used. This typically represents
// the device rebooting or being paired.
//
// The guard time is also checked when handling SLAAC address change
// events, to decide whether or not to request longer TX jitter.
mGuardTimer.Start(kGuardTimeAfterAttachToUseShorterTxJitter);
exit:
return;
}
void Client::ProcessAutoStart(void)
{
Ip6::SockAddr serverSockAddr;
DnsSrpAnycastInfo anycastInfo;
DnsSrpUnicastInfo unicastInfo;
AutoStart::State oldAutoStartState = mAutoStart.GetState();
bool shouldRestart = false;
// If auto start mode is enabled, we check the Network Data entries
// to discover and select the preferred SRP server to register with.
// If we currently have a selected server, we ensure that it is
// still present in the Network Data and is still the preferred one.
VerifyOrExit(mAutoStart.GetState() != AutoStart::kDisabled);
// If SRP client is running, we check to make sure that auto-start
// did select the current server, and server was not specified by
// user directly.
if (IsRunning())
{
VerifyOrExit(mAutoStart.HasSelectedServer());
}
// There are three types of entries in Network Data:
//
// 1) Preferred unicast entries with address included in service data.
// 2) Anycast entries (each having a seq number).
// 3) Unicast entries with address info included in server data.
serverSockAddr.Clear();
if (SelectUnicastEntry(NetworkData::Service::kAddrInServiceData, unicastInfo) == kErrorNone)
{
mAutoStart.SetState(AutoStart::kSelectedUnicastPreferred);
serverSockAddr = unicastInfo.mSockAddr;
}
else if (Get<NetworkData::Service::Manager>().FindPreferredDnsSrpAnycastInfo(anycastInfo) == kErrorNone)
{
serverSockAddr.SetAddress(anycastInfo.mAnycastAddress);
serverSockAddr.SetPort(kAnycastServerPort);
// We check if we are selecting an anycast entry for first
// time, or if the seq number has changed. Even if the
// anycast address remains the same as before, on a seq
// number change, the client still needs to restart to
// re-register its info.
if ((mAutoStart.GetState() != AutoStart::kSelectedAnycast) ||
(mAutoStart.GetAnycastSeqNum() != anycastInfo.mSequenceNumber))
{
shouldRestart = true;
mAutoStart.SetAnycastSeqNum(anycastInfo.mSequenceNumber);
}
mAutoStart.SetState(AutoStart::kSelectedAnycast);
}
else if (SelectUnicastEntry(NetworkData::Service::kAddrInServerData, unicastInfo) == kErrorNone)
{
mAutoStart.SetState(AutoStart::kSelectedUnicast);
serverSockAddr = unicastInfo.mSockAddr;
}
if (IsRunning())
{
VerifyOrExit((GetServerAddress() != serverSockAddr) || shouldRestart);
Stop(kRequesterAuto, kResetRetryInterval);
}
if (serverSockAddr.GetAddress().IsUnspecified())
{
if (mAutoStart.HasSelectedServer())
{
mAutoStart.SetState(AutoStart::kReselecting);
}
ExitNow();
}
// Before calling `Start()`, determine the trigger reason for
// starting the client with the newly discovered server based on
// `AutoStart` state transitions. This reason is then used to
// select the appropriate TX jitter interval (randomizing the
// initial SRP update transmission to the new server).
switch (oldAutoStartState)
{
case AutoStart::kDisabled:
break;
case AutoStart::kFirstTimeSelecting:
// If the device is attaching to an established Thread mesh
// (e.g., after a reboot or pairing), the Network Data it
// receives should already include a server entry, leading to
// a quick server selection after attachment. The `mGuardTimer`,
// started by `ApplyAutoStartGuardOnAttach()`, tracks a guard
// interval after the attach event. If server selection
// occurs within this short window, a shorter TX jitter is
// used (`TxJitter::kOnDeviceReboot`), allowing the device to
// register quickly and become discoverable.
//
// If server discovery takes longer, a longer TX jitter
// is used (`TxJitter::kOnServerStart`). This situation
// can indicate a server/BR starting up or a network-wide
// restart of many nodes (e.g., due to a power outage).
if (mGuardTimer.IsRunning())
{
mTxJitter.Request(TxJitter::kOnDeviceReboot);
}
else
{
mTxJitter.Request(TxJitter::kOnServerStart);
}
break;
case AutoStart::kReselecting:
// Server is restarted (or possibly a new server started).
mTxJitter.Request(TxJitter::kOnServerRestart);
break;
case AutoStart::kSelectedUnicastPreferred:
case AutoStart::kSelectedAnycast:
case AutoStart::kSelectedUnicast:
mTxJitter.Request(TxJitter::kOnServerSwitch);
break;
}
IgnoreError(Start(serverSockAddr, kRequesterAuto));
exit:
return;
}
Error Client::SelectUnicastEntry(DnsSrpUnicastType aType, DnsSrpUnicastInfo &aInfo) const
{
Error error = kErrorNotFound;
DnsSrpUnicastInfo unicastInfo;
NetworkData::Service::Iterator iterator(GetInstance());
#if OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
Settings::SrpClientInfo savedInfo;
bool hasSavedServerInfo = false;
if (!IsRunning())
{
hasSavedServerInfo = (Get<Settings>().Read(savedInfo) == kErrorNone);
}
#endif
while (iterator.GetNextDnsSrpUnicastInfo(aType, unicastInfo) == kErrorNone)
{
bool preferNewEntry;
if (mAutoStart.HasSelectedServer() && (GetServerAddress() == unicastInfo.mSockAddr))
{
aInfo = unicastInfo;
error = kErrorNone;
ExitNow();
}
#if OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
if (hasSavedServerInfo && (unicastInfo.mSockAddr.GetAddress() == savedInfo.GetServerAddress()) &&
(unicastInfo.mSockAddr.GetPort() == savedInfo.GetServerPort()))
{
// Stop the search if we see a match for the previously
// saved server info in the network data entries.
aInfo = unicastInfo;
error = kErrorNone;
ExitNow();
}
#endif
// Prefer the server with higher version number, if equal
// then pick the one with numerically smaller IPv6 address.
preferNewEntry = (error == kErrorNotFound) || (unicastInfo.mVersion > aInfo.mVersion);
if (!preferNewEntry && (unicastInfo.mVersion == aInfo.mVersion))
{
preferNewEntry = (unicastInfo.mSockAddr.GetAddress() < aInfo.mSockAddr.GetAddress());
}
if (preferNewEntry)
{
aInfo = unicastInfo;
error = kErrorNone;
}
}
exit:
return error;
}
#if OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
void Client::SelectNextServer(bool aDisallowSwitchOnRegisteredHost)
{
// This method tries to find the next unicast server info entry in the
// Network Data after the current one selected. If found, it
// restarts the client with the new server (keeping the retry wait
// interval as before).
Ip6::SockAddr serverSockAddr;
bool selectNext = false;
DnsSrpUnicastType type = NetworkData::Service::kAddrInServiceData;
serverSockAddr.Clear();
// Ensure that client is running, auto-start is enabled and
// auto-start selected the server and it is a unicast entry.
VerifyOrExit(IsRunning());
switch (mAutoStart.GetState())
{
case AutoStart::kSelectedUnicastPreferred:
type = NetworkData::Service::kAddrInServiceData;
break;
case AutoStart::kSelectedUnicast:
type = NetworkData::Service::kAddrInServerData;
break;
case AutoStart::kSelectedAnycast:
case AutoStart::kDisabled:
case AutoStart::kFirstTimeSelecting:
case AutoStart::kReselecting:
ExitNow();
}
if (aDisallowSwitchOnRegisteredHost)
{
// Ensure that host info is not yet registered (indicating that no
// service has yet been registered either).
VerifyOrExit((mHostInfo.GetState() == kAdding) || (mHostInfo.GetState() == kToAdd));
}
// We go through all entries to find the one matching the currently
// selected one, then set `selectNext` to `true` so to select the
// next one.
do
{
DnsSrpUnicastInfo unicastInfo;
NetworkData::Service::Iterator iterator(GetInstance());
while (iterator.GetNextDnsSrpUnicastInfo(type, unicastInfo) == kErrorNone)
{
if (selectNext)
{
serverSockAddr = unicastInfo.mSockAddr;
ExitNow();
}
if (GetServerAddress() == unicastInfo.mSockAddr)
{
selectNext = true;
}
}
// We loop back to handle the case where the current entry
// is the last one.
} while (selectNext);
// If we reach here it indicates we could not find the entry
// associated with currently selected server in the list. This
// situation is rather unlikely but can still happen if Network
// Data happens to be changed and the entry removed but
// the "changed" event from `Notifier` may have not yet been
// processed (note that events are emitted from their own
// tasklet). In such a case we keep `serverSockAddr` as empty.
exit:
if (!serverSockAddr.GetAddress().IsUnspecified() && (GetServerAddress() != serverSockAddr))
{
// We specifically update `mHostInfo` to `kToAdd` state. This
// ensures that `Stop()` will keep it as kToAdd` and we detect
// that the host info has not been registered yet and allow the
// `SelectNextServer()` to happen again if the timeouts/failures
// continue to happen with the new server.
mHostInfo.SetState(kToAdd);
Stop(kRequesterAuto, kKeepRetryInterval);
IgnoreError(Start(serverSockAddr, kRequesterAuto));
}
}
#endif // OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
#endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
const char *Client::ItemStateToString(ItemState aState)
{
#define ItemStateMapList(_) \
_(kToAdd, "ToAdd") \
_(kAdding, "Adding") \
_(kToRefresh, "ToRefresh") \
_(kRefreshing, "Refreshing") \
_(kToRemove, "ToRemove") \
_(kRemoving, "Removing") \
_(kRegistered, "Registered") \
_(kRemoved, "Removed")
DefineEnumStringArray(ItemStateMapList);
return kStrings[aState];
}
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
const char *Client::StateToString(State aState)
{
#define StateMapList(_) \
_(kStateStopped, "Stopped") \
_(kStatePaused, "Paused") \
_(kStateToUpdate, "ToUpdate") \
_(kStateUpdating, "Updating") \
_(kStateUpdated, "Updated") \
_(kStateToRetry, "ToRetry")
DefineEnumStringArray(StateMapList);
return kStrings[aState];
}
void Client::LogRetryWaitInterval(void) const
{
constexpr uint16_t kLogInMsecLimit = 5000; // Max interval (in msec) to log the value in msec unit
uint32_t interval = GetRetryWaitInterval();
LogInfo("Retry interval %lu %s", ToUlong((interval < kLogInMsecLimit) ? interval : Time::MsecToSec(interval)),
(interval < kLogInMsecLimit) ? "ms" : "sec");
}
#endif // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
} // namespace Srp
} // namespace ot
#endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE