| /* |
| * 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::SetAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses) |
| { |
| mAddresses = aAddresses; |
| mNumAddresses = aNumAddresses; |
| |
| 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) |
| #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE |
| , mServiceKeyRecordEnabled(false) |
| #endif |
| , mUpdateMessageId(0) |
| , mRetryWaitInterval(kMinRetryWaitInterval) |
| , mAcceptedLeaseInterval(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); |
| |
| // 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, |
| }; |
| |
| // 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 |
| } |
| |
| 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::SetHostAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses) |
| { |
| Error error = kErrorNone; |
| |
| VerifyOrExit((aAddresses != nullptr) && (aNumAddresses > 0), error = kErrorInvalidArgs); |
| |
| VerifyOrExit((mHostInfo.GetState() != kToRemove) && (mHostInfo.GetState() != kRemoving), |
| error = kErrorInvalidState); |
| |
| if (mHostInfo.GetState() == kRemoved) |
| { |
| mHostInfo.SetState(kToAdd); |
| } |
| else if (mHostInfo.GetState() != kToAdd) |
| { |
| mHostInfo.SetState(kToRefresh); |
| } |
| |
| mHostInfo.SetAddresses(aAddresses, aNumAddresses); |
| UpdateState(); |
| |
| 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) |
| { |
| 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); |
| |
| VerifyOrExit(message != nullptr, error = kErrorNoBufs); |
| 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)); |
| |
| 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)); |
| } |
| } |
| |
| 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 : mLeaseInterval); |
| 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(mLeaseInterval); |
| 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 |
| |
| exit: |
| return error; |
| } |
| |
| Error Client::AppendHostDescriptionInstruction(Message &aMessage, Info &aInfo) const |
| { |
| Error error = kErrorNone; |
| Dns::ResourceRecord rr; |
| |
| //---------------------------------- |
| // 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 |
| |
| rr.Init(Dns::ResourceRecord::kTypeAaaa); |
| rr.SetTtl(mLeaseInterval); |
| rr.SetLength(sizeof(Ip6::Address)); |
| |
| for (uint8_t index = 0; index < mHostInfo.GetNumAddresses(); index++) |
| { |
| SuccessOrExit(error = AppendHostName(aMessage, aInfo)); |
| SuccessOrExit(error = aMessage.Append(rr)); |
| SuccessOrExit(error = aMessage.Append(mHostInfo.GetAddress(index))); |
| aInfo.mRecordCount++; |
| } |
| |
| // KEY RR |
| |
| SuccessOrExit(error = AppendHostName(aMessage, aInfo)); |
| SuccessOrExit(error = AppendKeyRecord(aMessage, aInfo)); |
| |
| 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(mLeaseInterval); |
| 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); |
| |
| 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.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: |
| 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; |
| uint16_t numServers = 0; |
| #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 |
| numServers++; |
| |
| // Choose a server randomly (with uniform distribution) from |
| // the list of servers. As we iterate through server entries, |
| // with probability `1/numServers`, we choose to switch the |
| // current selected server with the new entry. This approach |
| // results in a uniform/same probability of selection among |
| // all server entries. |
| |
| if ((numServers == 1) || (Random::NonCrypto::GetUint16InRange(0, numServers) == 0)) |
| { |
| 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 |