2114 lines
64 KiB
C++
2114 lines
64 KiB
C++
/*
|
|
* 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 "common/as_core_type.hpp"
|
|
#include "common/code_utils.hpp"
|
|
#include "common/debug.hpp"
|
|
#include "common/instance.hpp"
|
|
#include "common/locator_getters.hpp"
|
|
#include "common/random.hpp"
|
|
#include "common/settings.hpp"
|
|
#include "common/string.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);
|
|
}
|
|
|
|
void Client::HostInfo::SetState(ItemState aState)
|
|
{
|
|
if (aState != GetState())
|
|
{
|
|
LogInfo("HostInfo %s -> %s", ItemStateToString(GetState()), ItemStateToString(aState));
|
|
mState = MapEnum(aState);
|
|
}
|
|
}
|
|
|
|
void Client::HostInfo::EnableAutoAddress(void)
|
|
{
|
|
mAddresses = nullptr;
|
|
mNumAddresses = 0;
|
|
mAutoAddress = true;
|
|
|
|
LogInfo("HostInfo enabled auto address", GetNumAddresses());
|
|
}
|
|
|
|
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;
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
void Client::Service::SetState(ItemState aState)
|
|
{
|
|
VerifyOrExit(GetState() != aState);
|
|
|
|
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);
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
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 (strcmp(GetName(), aOther.GetName()) == 0) && (strcmp(GetInstanceName(), aOther.GetInstanceName()) == 0);
|
|
}
|
|
|
|
//---------------------------------------------------------------------
|
|
// Client::AutoStart
|
|
|
|
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
|
|
|
|
Client::AutoStart::AutoStart(void)
|
|
{
|
|
Clear();
|
|
mState = kDefaultMode ? kSelectedNone : kDisabled;
|
|
}
|
|
|
|
bool Client::AutoStart::HasSelectedServer(void) const
|
|
{
|
|
bool hasSelected = false;
|
|
|
|
switch (mState)
|
|
{
|
|
case kDisabled:
|
|
case kSelectedNone:
|
|
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::SetCallback(AutoStartCallback aCallback, void *aContext)
|
|
{
|
|
mCallback = aCallback;
|
|
mContext = aContext;
|
|
}
|
|
|
|
void Client::AutoStart::InvokeCallback(const Ip6::SockAddr *aServerSockAddr) const
|
|
{
|
|
if (mCallback != nullptr)
|
|
{
|
|
mCallback(aServerSockAddr, mContext);
|
|
}
|
|
}
|
|
|
|
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
|
|
const char *Client::AutoStart::StateToString(State aState)
|
|
{
|
|
static const char *const kStateStrings[] = {
|
|
"Disabled", // (0) kDisabled
|
|
"Idle", // (1) kSelectedNone
|
|
"Unicast-prf", // (2) kSelectedUnicastPreferred
|
|
"Anycast", // (3) kSelectedAnycast
|
|
"Unicast", // (4) kSelectedUnicast
|
|
};
|
|
|
|
static_assert(0 == kDisabled, "kDisabled value is incorrect");
|
|
static_assert(1 == kSelectedNone, "kSelectedNone value is incorrect");
|
|
static_assert(2 == kSelectedUnicastPreferred, "kSelectedUnicastPreferred value is incorrect");
|
|
static_assert(3 == kSelectedAnycast, "kSelectedAnycast value is incorrect");
|
|
static_assert(4 == kSelectedUnicast, "kSelectedUnicast value is incorrect");
|
|
|
|
return kStateStrings[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)
|
|
, mAutoHostAddressAddedMeshLocal(false)
|
|
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
|
|
, mServiceKeyRecordEnabled(false)
|
|
#endif
|
|
, mUpdateMessageId(0)
|
|
, mRetryWaitInterval(kMinRetryWaitInterval)
|
|
, mAcceptedLeaseInterval(0)
|
|
, mTtl(0)
|
|
, mLeaseInterval(kDefaultLease)
|
|
, mKeyLeaseInterval(kDefaultKeyLease)
|
|
, mSocket(aInstance)
|
|
, mCallback(nullptr)
|
|
, mCallbackContext(nullptr)
|
|
, mDomainName(kDefaultDomainName)
|
|
, mTimer(aInstance, Client::HandleTimer)
|
|
{
|
|
mHostInfo.Init();
|
|
|
|
// 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.
|
|
|
|
static_assert(kToAdd == 0, "kToAdd value is not correct");
|
|
static_assert(kAdding == 1, "kAdding value is not correct");
|
|
static_assert(kToRefresh == 2, "kToRefresh value is not correct");
|
|
static_assert(kRefreshing == 3, "kRefreshing value is not correct");
|
|
static_assert(kToRemove == 4, "kToRemove value is not correct");
|
|
static_assert(kRemoving == 5, "kRemoving value is not correct");
|
|
static_assert(kRegistered == 6, "kRegistered value is not correct");
|
|
static_assert(kRemoved == 7, "kRemoved value is not correct");
|
|
}
|
|
|
|
Error Client::Start(const Ip6::SockAddr &aServerSockAddr, Requester aRequester)
|
|
{
|
|
Error error;
|
|
|
|
VerifyOrExit(GetState() == kStateStopped,
|
|
error = (aServerSockAddr == GetServerAddress()) ? kErrorNone : kErrorBusy);
|
|
|
|
SuccessOrExit(error = mSocket.Open(Client::HandleUdpReceive, this));
|
|
SuccessOrExit(error = mSocket.Connect(aServerSockAddr));
|
|
|
|
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);
|
|
|
|
mSingleServiceMode.Disable();
|
|
|
|
// State changes:
|
|
// kAdding -> kToRefresh
|
|
// kRefreshing -> kToRefresh
|
|
// kRemoving -> kToRemove
|
|
// kRegistered -> kToRefresh
|
|
|
|
ChangeHostAndServiceStates(kNewStateOnStop);
|
|
|
|
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.ResetTimoutFailureCount();
|
|
#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::SetCallback(Callback aCallback, void *aContext)
|
|
{
|
|
mCallback = aCallback;
|
|
mCallbackContext = aContext;
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
mSingleServiceMode.Disable();
|
|
|
|
// State changes:
|
|
// kAdding -> kToRefresh
|
|
// kRefreshing -> kToRefresh
|
|
// kRemoving -> kToRemove
|
|
|
|
ChangeHostAndServiceStates(kNewStateOnPause);
|
|
|
|
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 (mHostInfo.IsAutoAddressEnabled())
|
|
{
|
|
Events::Flags eventFlags = (kEventIp6AddressAdded | kEventIp6AddressRemoved);
|
|
|
|
if (mAutoHostAddressAddedMeshLocal)
|
|
{
|
|
eventFlags |= kEventThreadMeshLocalAddrChanged;
|
|
}
|
|
|
|
if (aEvents.ContainsAny(eventFlags))
|
|
{
|
|
IgnoreError(UpdateHostInfoStateOnAddressChange());
|
|
UpdateState();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Client::HandleRoleChanged(void)
|
|
{
|
|
if (Get<Mle::Mle>().IsAttached())
|
|
{
|
|
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());
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
|
|
// Check if the service was removed immediately, if so
|
|
// invoke the callback to report the removed service.
|
|
GetRemovedServices(removedServices);
|
|
|
|
if (!removedServices.IsEmpty())
|
|
{
|
|
InvokeCallback(kErrorNone, mHostInfo, removedServices.GetHead());
|
|
}
|
|
|
|
UpdateState();
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
void Client::UpdateServiceStateToRemove(Service &aService)
|
|
{
|
|
if (aService.GetState() == kToAdd)
|
|
{
|
|
// If the service has not been added yet, we can remove it immediately.
|
|
aService.SetState(kRemoved);
|
|
}
|
|
else 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(kUpdateTxDelay);
|
|
break;
|
|
|
|
case kStateUpdating:
|
|
mTimer.Start(GetRetryWaitInterval());
|
|
break;
|
|
|
|
case kStateToRetry:
|
|
break;
|
|
}
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
void Client::ChangeHostAndServiceStates(const ItemState *aNewStates)
|
|
{
|
|
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
|
|
ItemState oldHostState = mHostInfo.GetState();
|
|
#endif
|
|
|
|
mHostInfo.SetState(aNewStates[mHostInfo.GetState()]);
|
|
|
|
for (Service &service : mServices)
|
|
{
|
|
if (mSingleServiceMode.IsEnabled() && mSingleServiceMode.GetService() != &service)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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::kSelectedNone:
|
|
break;
|
|
|
|
case AutoStart::kSelectedUnicastPreferred:
|
|
case AutoStart::kSelectedUnicast:
|
|
info.SetServerAddress(GetServerAddress().GetAddress());
|
|
info.SetServerPort(GetServerAddress().GetPort());
|
|
IgnoreError(Get<Settings>().Save(info));
|
|
break;
|
|
|
|
case AutoStart::kSelectedAnycast:
|
|
IgnoreError(Get<Settings>().Delete<Settings::SrpClientInfo>());
|
|
break;
|
|
}
|
|
}
|
|
#endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
|
|
}
|
|
|
|
void Client::InvokeCallback(Error aError) const
|
|
{
|
|
InvokeCallback(aError, mHostInfo, nullptr);
|
|
}
|
|
|
|
void Client::InvokeCallback(Error aError, const HostInfo &aHostInfo, const Service *aRemovedServices) const
|
|
{
|
|
VerifyOrExit(mCallback != nullptr);
|
|
mCallback(aError, &aHostInfo, mServices.GetHead(), aRemovedServices, mCallbackContext);
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
Error error = kErrorNone;
|
|
Message *message = mSocket.NewMessage(0);
|
|
uint32_t length;
|
|
|
|
VerifyOrExit(message != nullptr, error = kErrorNoBufs);
|
|
SuccessOrExit(error = PrepareUpdateMessage(*message));
|
|
|
|
length = message->GetLength() + sizeof(Ip6::Udp::Header) + sizeof(Ip6::Header);
|
|
|
|
if (length >= Ip6::kMaxDatagramLength)
|
|
{
|
|
LogInfo("Msg len %u is larger than MTU, enabling single service mode", length);
|
|
mSingleServiceMode.Enable();
|
|
IgnoreError(message->SetLength(0));
|
|
SuccessOrExit(error = PrepareUpdateMessage(*message));
|
|
}
|
|
|
|
SuccessOrExit(error = mSocket.SendTo(*message, Ip6::MessageInfo()));
|
|
|
|
LogInfo("Send update");
|
|
|
|
// State changes:
|
|
// kToAdd -> kAdding
|
|
// kToRefresh -> kRefreshing
|
|
// kToRemove -> kRemoving
|
|
|
|
ChangeHostAndServiceStates(kNewStateOnMessageTx);
|
|
|
|
// 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).
|
|
|
|
LogInfo("Failed to send update: %s", ErrorToString(error));
|
|
|
|
mSingleServiceMode.Disable();
|
|
FreeMessage(message);
|
|
|
|
SetState(kStateToRetry);
|
|
|
|
if (mTxFailureRetryCount < kMaxTxFailureRetries)
|
|
{
|
|
uint32_t interval;
|
|
|
|
mTxFailureRetryCount++;
|
|
interval = Random::NonCrypto::AddJitter(kTxFailureRetryInterval, kTxFailureRetryJitter);
|
|
mTimer.Start(interval);
|
|
|
|
LogInfo("Quick retry %d in %u msec", mTxFailureRetryCount, interval);
|
|
|
|
// Do not report message preparation errors to user
|
|
// until `kMaxTxFailureRetries` are exhausted.
|
|
}
|
|
else
|
|
{
|
|
LogRetryWaitInterval();
|
|
mTimer.Start(Random::NonCrypto::AddJitter(GetRetryWaitInterval(), kRetryIntervalJitter));
|
|
GrowRetryWaitInterval();
|
|
InvokeCallback(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
Error Client::PrepareUpdateMessage(Message &aMessage)
|
|
{
|
|
constexpr uint16_t kHeaderOffset = 0;
|
|
|
|
Error error = kErrorNone;
|
|
Dns::UpdateHeader header;
|
|
Info info;
|
|
|
|
info.Clear();
|
|
|
|
SuccessOrExit(error = ReadOrGenerateKey(info.mKeyPair));
|
|
|
|
// Generate random Message ID and ensure it is different from last one
|
|
do
|
|
{
|
|
SuccessOrExit(error = header.SetRandomMessageId());
|
|
} while (header.GetMessageId() == mUpdateMessageId);
|
|
|
|
mUpdateMessageId = header.GetMessageId();
|
|
|
|
// 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 = aMessage.Append(header));
|
|
|
|
// Prepare Zone section
|
|
|
|
info.mDomainNameOffset = aMessage.GetLength();
|
|
SuccessOrExit(error = Dns::Name::AppendName(mDomainName, aMessage));
|
|
SuccessOrExit(error = aMessage.Append(Dns::Zone()));
|
|
|
|
// Prepare Update section
|
|
|
|
if ((mHostInfo.GetState() != kToRemove) && (mHostInfo.GetState() != kRemoving))
|
|
{
|
|
for (Service &service : mServices)
|
|
{
|
|
SuccessOrExit(error = AppendServiceInstructions(service, aMessage, info));
|
|
|
|
if (mSingleServiceMode.IsEnabled() && (mSingleServiceMode.GetService() != nullptr))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
SuccessOrExit(error = AppendHostDescriptionInstruction(aMessage, info));
|
|
|
|
header.SetUpdateRecordCount(info.mRecordCount);
|
|
aMessage.Write(kHeaderOffset, header);
|
|
|
|
// Prepare Additional Data section
|
|
|
|
SuccessOrExit(error = AppendUpdateLeaseOptRecord(aMessage));
|
|
SuccessOrExit(error = AppendSignature(aMessage, info));
|
|
|
|
header.SetAdditionalRecordCount(2); // Lease OPT and SIG RRs
|
|
aMessage.Write(kHeaderOffset, header);
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
Error Client::ReadOrGenerateKey(Crypto::Ecdsa::P256::KeyPair &aKeyPair)
|
|
{
|
|
Error error;
|
|
|
|
error = Get<Settings>().Read<Settings::SrpEcdsaKey>(aKeyPair);
|
|
|
|
if (error == kErrorNone)
|
|
{
|
|
Crypto::Ecdsa::P256::PublicKey publicKey;
|
|
|
|
if (aKeyPair.GetPublicKey(publicKey) == kErrorNone)
|
|
{
|
|
ExitNow();
|
|
}
|
|
}
|
|
|
|
SuccessOrExit(error = aKeyPair.Generate());
|
|
IgnoreError(Get<Settings>().Save<Settings::SrpEcdsaKey>(aKeyPair));
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
Error Client::AppendServiceInstructions(Service &aService, Message &aMessage, Info &aInfo)
|
|
{
|
|
Error error = kErrorNone;
|
|
Dns::ResourceRecord rr;
|
|
Dns::SrvRecord srv;
|
|
bool removing;
|
|
uint16_t serviceNameOffset;
|
|
uint16_t instanceNameOffset;
|
|
uint16_t offset;
|
|
|
|
if (aService.GetState() == kRegistered)
|
|
{
|
|
// 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.
|
|
|
|
VerifyOrExit(ShouldRenewEarly(aService));
|
|
aService.SetState(kToRefresh);
|
|
}
|
|
|
|
removing = ((aService.GetState() == kToRemove) || (aService.GetState() == kRemoving));
|
|
|
|
//----------------------------------
|
|
// Service Discovery Instruction
|
|
|
|
// PTR record
|
|
|
|
// "service name labels" + (pointer to) domain name.
|
|
serviceNameOffset = aMessage.GetLength();
|
|
SuccessOrExit(error = Dns::Name::AppendMultipleLabels(aService.GetName(), aMessage));
|
|
SuccessOrExit(error = Dns::Name::AppendPointerLabel(aInfo.mDomainNameOffset, aMessage));
|
|
|
|
// 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 : GetTtl());
|
|
offset = aMessage.GetLength();
|
|
SuccessOrExit(error = aMessage.Append(rr));
|
|
|
|
// "Instance name" + (pointer to) service name.
|
|
instanceNameOffset = aMessage.GetLength();
|
|
SuccessOrExit(error = Dns::Name::AppendLabel(aService.GetInstanceName(), aMessage));
|
|
SuccessOrExit(error = Dns::Name::AppendPointerLabel(serviceNameOffset, aMessage));
|
|
|
|
UpdateRecordLengthInMessage(rr, offset, aMessage);
|
|
aInfo.mRecordCount++;
|
|
|
|
if (aService.HasSubType())
|
|
{
|
|
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, aMessage));
|
|
|
|
if (index == 0)
|
|
{
|
|
subServiceNameOffset = aMessage.GetLength();
|
|
SuccessOrExit(error = Dns::Name::AppendLabel("_sub", aMessage));
|
|
SuccessOrExit(error = Dns::Name::AppendPointerLabel(serviceNameOffset, aMessage));
|
|
}
|
|
else
|
|
{
|
|
SuccessOrExit(error = Dns::Name::AppendPointerLabel(subServiceNameOffset, aMessage));
|
|
}
|
|
|
|
// `rr` is already initialized as PTR (add or remove).
|
|
offset = aMessage.GetLength();
|
|
SuccessOrExit(error = aMessage.Append(rr));
|
|
|
|
SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage));
|
|
UpdateRecordLengthInMessage(rr, offset, aMessage);
|
|
aInfo.mRecordCount++;
|
|
}
|
|
}
|
|
|
|
//----------------------------------
|
|
// Service Description Instruction
|
|
|
|
// "Delete all RRsets from a name" for Instance Name.
|
|
|
|
SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage));
|
|
SuccessOrExit(error = AppendDeleteAllRrsets(aMessage));
|
|
aInfo.mRecordCount++;
|
|
|
|
VerifyOrExit(!removing);
|
|
|
|
// SRV RR
|
|
|
|
SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage));
|
|
srv.Init();
|
|
srv.SetTtl(GetTtl());
|
|
srv.SetPriority(aService.GetPriority());
|
|
srv.SetWeight(aService.GetWeight());
|
|
srv.SetPort(aService.GetPort());
|
|
offset = aMessage.GetLength();
|
|
SuccessOrExit(error = aMessage.Append(srv));
|
|
SuccessOrExit(error = AppendHostName(aMessage, aInfo));
|
|
UpdateRecordLengthInMessage(srv, offset, aMessage);
|
|
aInfo.mRecordCount++;
|
|
|
|
// TXT RR
|
|
|
|
SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage));
|
|
rr.Init(Dns::ResourceRecord::kTypeTxt);
|
|
offset = aMessage.GetLength();
|
|
SuccessOrExit(error = aMessage.Append(rr));
|
|
SuccessOrExit(error =
|
|
Dns::TxtEntry::AppendEntries(aService.GetTxtEntries(), aService.GetNumTxtEntries(), aMessage));
|
|
UpdateRecordLengthInMessage(rr, offset, aMessage);
|
|
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, aMessage));
|
|
SuccessOrExit(error = AppendKeyRecord(aMessage, aInfo));
|
|
}
|
|
#endif
|
|
|
|
if (mSingleServiceMode.IsEnabled())
|
|
{
|
|
mSingleServiceMode.SetService(aService);
|
|
}
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
Error Client::AppendHostDescriptionInstruction(Message &aMessage, Info &aInfo)
|
|
{
|
|
Error error = kErrorNone;
|
|
|
|
//----------------------------------
|
|
// Host Description Instruction
|
|
|
|
// "Delete all RRsets from a name" for Host Name.
|
|
|
|
SuccessOrExit(error = AppendHostName(aMessage, aInfo));
|
|
SuccessOrExit(error = AppendDeleteAllRrsets(aMessage));
|
|
aInfo.mRecordCount++;
|
|
|
|
// AAAA RRs
|
|
|
|
if (mHostInfo.IsAutoAddressEnabled())
|
|
{
|
|
// Append all addresses on Thread netif excluding link-local and
|
|
// mesh-local addresses. If no address is appended, we include
|
|
// the mesh local address.
|
|
|
|
mAutoHostAddressAddedMeshLocal = true;
|
|
|
|
for (const Ip6::Netif::UnicastAddress &unicastAddress : Get<ThreadNetif>().GetUnicastAddresses())
|
|
{
|
|
if (unicastAddress.GetAddress().IsLinkLocal() ||
|
|
Get<Mle::Mle>().IsMeshLocalAddress(unicastAddress.GetAddress()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
SuccessOrExit(error = AppendAaaaRecord(unicastAddress.GetAddress(), aMessage, aInfo));
|
|
mAutoHostAddressAddedMeshLocal = false;
|
|
}
|
|
|
|
if (mAutoHostAddressAddedMeshLocal)
|
|
{
|
|
SuccessOrExit(error = AppendAaaaRecord(Get<Mle::Mle>().GetMeshLocal64(), aMessage, aInfo));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (uint8_t index = 0; index < mHostInfo.GetNumAddresses(); index++)
|
|
{
|
|
SuccessOrExit(error = AppendAaaaRecord(mHostInfo.GetAddress(index), aMessage, aInfo));
|
|
}
|
|
}
|
|
|
|
// KEY RR
|
|
|
|
SuccessOrExit(error = AppendHostName(aMessage, aInfo));
|
|
SuccessOrExit(error = AppendKeyRecord(aMessage, aInfo));
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
Error Client::AppendAaaaRecord(const Ip6::Address &aAddress, Message &aMessage, Info &aInfo) const
|
|
{
|
|
Error error;
|
|
Dns::ResourceRecord rr;
|
|
|
|
rr.Init(Dns::ResourceRecord::kTypeAaaa);
|
|
rr.SetTtl(GetTtl());
|
|
rr.SetLength(sizeof(Ip6::Address));
|
|
|
|
SuccessOrExit(error = AppendHostName(aMessage, aInfo));
|
|
SuccessOrExit(error = aMessage.Append(rr));
|
|
SuccessOrExit(error = aMessage.Append(aAddress));
|
|
aInfo.mRecordCount++;
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
Error Client::AppendKeyRecord(Message &aMessage, Info &aInfo) const
|
|
{
|
|
Error error;
|
|
Dns::KeyRecord key;
|
|
Crypto::Ecdsa::P256::PublicKey publicKey;
|
|
|
|
key.Init();
|
|
key.SetTtl(GetTtl());
|
|
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 = aMessage.Append(key));
|
|
SuccessOrExit(error = aInfo.mKeyPair.GetPublicKey(publicKey));
|
|
SuccessOrExit(error = aMessage.Append(publicKey));
|
|
aInfo.mRecordCount++;
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
Error Client::AppendDeleteAllRrsets(Message &aMessage) 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 aMessage.Append(rr);
|
|
}
|
|
|
|
Error Client::AppendHostName(Message &aMessage, Info &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(), aMessage));
|
|
error = Dns::Name::AppendName(mDomainName, aMessage);
|
|
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 != Info::kUnknownOffset)
|
|
{
|
|
ExitNow(error = Dns::Name::AppendPointerLabel(aInfo.mHostNameOffset, aMessage));
|
|
}
|
|
|
|
aInfo.mHostNameOffset = aMessage.GetLength();
|
|
SuccessOrExit(error = Dns::Name::AppendMultipleLabels(mHostInfo.GetName(), aMessage));
|
|
error = Dns::Name::AppendPointerLabel(aInfo.mDomainNameOffset, aMessage);
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
Error Client::AppendUpdateLeaseOptRecord(Message &aMessage) const
|
|
{
|
|
Error error;
|
|
Dns::OptRecord optRecord;
|
|
Dns::LeaseOption leaseOption;
|
|
|
|
// Append empty (root domain) as OPT RR name.
|
|
SuccessOrExit(error = Dns::Name::AppendTerminator(aMessage));
|
|
|
|
// `Init()` sets the type and clears (set to zero) the extended
|
|
// Response Code, version and all flags.
|
|
optRecord.Init();
|
|
optRecord.SetUdpPayloadSize(kUdpPayloadSize);
|
|
optRecord.SetDnsSecurityFlag();
|
|
optRecord.SetLength(sizeof(Dns::LeaseOption));
|
|
|
|
SuccessOrExit(error = aMessage.Append(optRecord));
|
|
|
|
leaseOption.Init();
|
|
|
|
if ((mHostInfo.GetState() == kToRemove) || (mHostInfo.GetState() == kRemoving))
|
|
{
|
|
leaseOption.SetLeaseInterval(0);
|
|
leaseOption.SetKeyLeaseInterval(mShouldRemoveKeyLease ? 0 : mKeyLeaseInterval);
|
|
}
|
|
else
|
|
{
|
|
leaseOption.SetLeaseInterval(mLeaseInterval);
|
|
leaseOption.SetKeyLeaseInterval(mKeyLeaseInterval);
|
|
}
|
|
|
|
error = aMessage.Append(leaseOption);
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
Error Client::AppendSignature(Message &aMessage, Info &aInfo)
|
|
{
|
|
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);
|
|
|
|
// 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 = aMessage.GetLength();
|
|
SuccessOrExit(error = aMessage.Append(sig));
|
|
SuccessOrExit(error = AppendHostName(aMessage, 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 = aMessage.GetLength() - offset - sizeof(Dns::ResourceRecord);
|
|
sha256.Update(aMessage, offset + sizeof(Dns::ResourceRecord), len);
|
|
|
|
// (2) Message from DNS header before SIG
|
|
sha256.Update(aMessage, 0, offset);
|
|
|
|
sha256.Finish(hash);
|
|
SuccessOrExit(error = aInfo.mKeyPair.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(aMessage.SetLength(offset));
|
|
|
|
// SIG(0) uses owner name of root (single zero byte).
|
|
SuccessOrExit(error = Dns::Name::AppendTerminator(aMessage));
|
|
|
|
offset = aMessage.GetLength();
|
|
SuccessOrExit(error = aMessage.Append(sig));
|
|
SuccessOrExit(error = AppendHostName(aMessage, aInfo));
|
|
SuccessOrExit(error = aMessage.Append(signature));
|
|
UpdateRecordLengthInMessage(sig, offset, aMessage);
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
void Client::UpdateRecordLengthInMessage(Dns::ResourceRecord &aRecord, uint16_t aOffset, Message &aMessage) const
|
|
{
|
|
// This method is used to calculate an RR DATA length and update
|
|
// (rewrite) it in a message. This should be called immediately
|
|
// after all the fields in the record are written in the message.
|
|
// `aOffset` gives the offset in the message to the start of the
|
|
// record.
|
|
|
|
aRecord.SetLength(aMessage.GetLength() - aOffset - sizeof(Dns::ResourceRecord));
|
|
aMessage.Write(aOffset, aRecord);
|
|
}
|
|
|
|
void Client::HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
|
|
{
|
|
OT_UNUSED_VARIABLE(aMessageInfo);
|
|
|
|
static_cast<Client *>(aContext)->ProcessResponse(AsCoreType(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;
|
|
|
|
VerifyOrExit(GetState() == kStateUpdating);
|
|
|
|
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() == mUpdateMessageId, error = kErrorDrop);
|
|
|
|
if (!Get<Mle::Mle>().IsRxOnWhenIdle())
|
|
{
|
|
Get<DataPollSender>().StopFastPolls();
|
|
}
|
|
|
|
// Response is for the earlier request message.
|
|
|
|
LogInfo("Received response");
|
|
|
|
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
|
|
mAutoStart.ResetTimoutFailureCount();
|
|
#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.
|
|
|
|
mAcceptedLeaseInterval = mLeaseInterval;
|
|
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 the guard time) we
|
|
// just use half of the accepted lease interval.
|
|
|
|
if (mAcceptedLeaseInterval > kLeaseRenewGuardInterval)
|
|
{
|
|
mLeaseRenewTime += Time::SecToMsec(mAcceptedLeaseInterval - kLeaseRenewGuardInterval);
|
|
}
|
|
else
|
|
{
|
|
mLeaseRenewTime += Time::SecToMsec(mAcceptedLeaseInterval) / 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);
|
|
mSingleServiceMode.Disable();
|
|
|
|
HandleUpdateDone();
|
|
UpdateState();
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogInfo("Failed to process response %s", ErrorToString(error));
|
|
}
|
|
}
|
|
|
|
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(kRemoved, aRemovedServices);
|
|
}
|
|
|
|
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;
|
|
uint16_t len;
|
|
|
|
IgnoreError(Dns::Name::ParseName(aMessage, aOffset));
|
|
aOffset += sizeof(Dns::OptRecord);
|
|
|
|
len = aOptRecord.GetLength();
|
|
|
|
while (len > 0)
|
|
{
|
|
Dns::LeaseOption leaseOption;
|
|
Dns::Option & option = leaseOption;
|
|
uint16_t size;
|
|
|
|
SuccessOrExit(error = aMessage.Read(aOffset, option));
|
|
|
|
VerifyOrExit(aOffset + option.GetSize() <= aMessage.GetLength(), error = kErrorParse);
|
|
|
|
if ((option.GetOptionCode() == Dns::Option::kUpdateLease) &&
|
|
(option.GetOptionLength() >= Dns::LeaseOption::kOptionLength))
|
|
{
|
|
SuccessOrExit(error = aMessage.Read(aOffset, leaseOption));
|
|
|
|
mAcceptedLeaseInterval = leaseOption.GetLeaseInterval();
|
|
|
|
if (mAcceptedLeaseInterval > kMaxLease)
|
|
{
|
|
mAcceptedLeaseInterval = kMaxLease;
|
|
}
|
|
}
|
|
|
|
size = static_cast<uint16_t>(option.GetSize());
|
|
aOffset += size;
|
|
len -= size;
|
|
}
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
void Client::UpdateState(void)
|
|
{
|
|
TimeMilli now = TimerMilli::GetNow();
|
|
TimeMilli earliestRenewTime = now.GetDistantFuture();
|
|
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 (now < mLeaseRenewTime)
|
|
{
|
|
break;
|
|
}
|
|
|
|
mHostInfo.SetState(kToRefresh);
|
|
|
|
// 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)));
|
|
|
|
// 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() <= now)
|
|
{
|
|
service.SetState(kToRefresh);
|
|
shouldUpdate = true;
|
|
}
|
|
else if (service.GetLeaseRenewTime() < earliestRenewTime)
|
|
{
|
|
earliestRenewTime = service.GetLeaseRenewTime();
|
|
}
|
|
|
|
break;
|
|
|
|
case kAdding:
|
|
case kRefreshing:
|
|
case kRemoving:
|
|
case kRemoved:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldUpdate)
|
|
{
|
|
SetState(kStateToUpdate);
|
|
ExitNow();
|
|
}
|
|
|
|
if ((GetState() == kStateUpdated) && (earliestRenewTime != now.GetDistantFuture()))
|
|
{
|
|
mTimer.FireAt(earliestRenewTime);
|
|
}
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
void Client::GrowRetryWaitInterval(void)
|
|
{
|
|
mRetryWaitInterval =
|
|
mRetryWaitInterval / kRetryIntervalGrowthFactorDenominator * kRetryIntervalGrowthFactorNumerator;
|
|
|
|
if (mRetryWaitInterval > kMaxRetryWaitInterval)
|
|
{
|
|
mRetryWaitInterval = kMaxRetryWaitInterval;
|
|
}
|
|
}
|
|
|
|
uint32_t Client::GetBoundedLeaseInterval(uint32_t aInterval, uint32_t aDefaultInterval) const
|
|
{
|
|
uint32_t boundedInterval = aDefaultInterval;
|
|
|
|
if (aInterval != 0)
|
|
{
|
|
boundedInterval = OT_MIN(aInterval, static_cast<uint32_t>(kMaxLease));
|
|
}
|
|
|
|
return boundedInterval;
|
|
}
|
|
|
|
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 `mAcceptedLeaseInterval`. 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 =
|
|
Time::SecToMsec(mAcceptedLeaseInterval) / kEarlyLeaseRenewFactorDenominator * kEarlyLeaseRenewFactorNumerator;
|
|
|
|
shouldRenew = (aService.GetLeaseRenewTime() <= TimerMilli::GetNow() + earlyRenewInterval);
|
|
#else
|
|
OT_UNUSED_VARIABLE(aService);
|
|
shouldRenew = true;
|
|
#endif
|
|
|
|
return shouldRenew;
|
|
}
|
|
|
|
void Client::HandleTimer(Timer &aTimer)
|
|
{
|
|
aTimer.Get<Client>().HandleTimer();
|
|
}
|
|
|
|
void Client::HandleTimer(void)
|
|
{
|
|
switch (GetState())
|
|
{
|
|
case kStateStopped:
|
|
case kStatePaused:
|
|
break;
|
|
|
|
case kStateToUpdate:
|
|
case kStateToRetry:
|
|
SendUpdate();
|
|
break;
|
|
|
|
case kStateUpdating:
|
|
mSingleServiceMode.Disable();
|
|
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.IncrementTimoutFailureCount();
|
|
|
|
if (mAutoStart.GetTimoutFailureCount() >= 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::kSelectedNone);
|
|
ProcessAutoStart();
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
void Client::ProcessAutoStart(void)
|
|
{
|
|
Ip6::SockAddr serverSockAddr;
|
|
DnsSrpAnycast::Info anycastInfo;
|
|
DnsSrpUnicast::Info unicastInfo;
|
|
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.GetState() != AutoStart::kSelectedNone);
|
|
}
|
|
|
|
// 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(DnsSrpUnicast::kFromServiceData, 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(DnsSrpUnicast::kFromServerData, unicastInfo) == kErrorNone)
|
|
{
|
|
mAutoStart.SetState(AutoStart::kSelectedUnicast);
|
|
serverSockAddr = unicastInfo.mSockAddr;
|
|
}
|
|
|
|
if (IsRunning())
|
|
{
|
|
VerifyOrExit((GetServerAddress() != serverSockAddr) || shouldRestart);
|
|
Stop(kRequesterAuto, kResetRetryInterval);
|
|
}
|
|
|
|
if (!serverSockAddr.GetAddress().IsUnspecified())
|
|
{
|
|
IgnoreError(Start(serverSockAddr, kRequesterAuto));
|
|
}
|
|
else
|
|
{
|
|
mAutoStart.SetState(AutoStart::kSelectedNone);
|
|
}
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
Error Client::SelectUnicastEntry(DnsSrpUnicast::Origin aOrigin, DnsSrpUnicast::Info &aInfo) const
|
|
{
|
|
Error error = kErrorNotFound;
|
|
DnsSrpUnicast::Info unicastInfo;
|
|
NetworkData::Service::Manager::Iterator iterator;
|
|
#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 (Get<NetworkData::Service::Manager>().GetNextDnsSrpUnicastInfo(iterator, unicastInfo) == kErrorNone)
|
|
{
|
|
if (unicastInfo.mOrigin != aOrigin)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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 numerically lowest server address
|
|
|
|
if ((error == kErrorNotFound) || (unicastInfo.mSockAddr.GetAddress() < aInfo.mSockAddr.GetAddress()))
|
|
{
|
|
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;
|
|
DnsSrpUnicast::Origin origin = DnsSrpUnicast::kFromServiceData;
|
|
|
|
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:
|
|
origin = DnsSrpUnicast::kFromServiceData;
|
|
break;
|
|
|
|
case AutoStart::kSelectedUnicast:
|
|
origin = DnsSrpUnicast::kFromServerData;
|
|
break;
|
|
|
|
case AutoStart::kSelectedAnycast:
|
|
case AutoStart::kDisabled:
|
|
case AutoStart::kSelectedNone:
|
|
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
|
|
{
|
|
DnsSrpUnicast::Info unicastInfo;
|
|
NetworkData::Service::Manager::Iterator iterator;
|
|
|
|
while (Get<NetworkData::Service::Manager>().GetNextDnsSrpUnicastInfo(iterator, unicastInfo) == kErrorNone)
|
|
{
|
|
if (unicastInfo.mOrigin != origin)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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)
|
|
{
|
|
static const char *const kItemStateStrings[] = {
|
|
"ToAdd", // kToAdd (0)
|
|
"Adding", // kAdding (1)
|
|
"ToRefresh", // kToRefresh (2)
|
|
"Refreshing", // kRefreshing (3)
|
|
"ToRemove", // kToRemove (4)
|
|
"Removing", // kRemoving (5)
|
|
"Registered", // kRegistered (6)
|
|
"Removed", // kRemoved (7)
|
|
};
|
|
|
|
return kItemStateStrings[aState];
|
|
}
|
|
|
|
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
|
|
|
|
const char *Client::StateToString(State aState)
|
|
{
|
|
static const char *const kStateStrings[] = {
|
|
"Stopped", // kStateStopped (0)
|
|
"Paused", // kStatePaused (1)
|
|
"ToUpdate", // kStateToUpdate (2)
|
|
"Updating", // kStateUpdating (3)
|
|
"Updated", // kStateUpdated (4)
|
|
"ToRetry", // kStateToRetry (5)
|
|
};
|
|
|
|
static_assert(kStateStopped == 0, "kStateStopped value is not correct");
|
|
static_assert(kStatePaused == 1, "kStatePaused value is not correct");
|
|
static_assert(kStateToUpdate == 2, "kStateToUpdate value is not correct");
|
|
static_assert(kStateUpdating == 3, "kStateUpdating value is not correct");
|
|
static_assert(kStateUpdated == 4, "kStateUpdated value is not correct");
|
|
static_assert(kStateToRetry == 5, "kStateToRetry value is not correct");
|
|
|
|
return kStateStrings[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 %u %s", (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
|