/* * Copyright (c) 2024, 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 "mdns.hpp" #if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE #include "common/crc.hpp" #include "instance/instance.hpp" /** * @file * This file implements the Multicast DNS (mDNS) per RFC 6762. */ namespace ot { namespace Dns { namespace Multicast { RegisterLogModule("MulticastDns"); //--------------------------------------------------------------------------------------------------------------------- // otPlatMdns callbacks extern "C" void otPlatMdnsHandleReceive(otInstance *aInstance, otMessage *aMessage, bool aIsUnicast, const otPlatMdnsAddressInfo *aAddress) { AsCoreType(aInstance).Get().HandleMessage(AsCoreType(aMessage), aIsUnicast, AsCoreType(aAddress)); } //---------------------------------------------------------------------------------------------------------------------- // Core const char Core::kLocalDomain[] = "local."; const char Core::kUdpServiceLabel[] = "_udp"; const char Core::kTcpServiceLabel[] = "_tcp"; const char Core::kSubServiceLabel[] = "_sub"; const char Core::kServicesDnssdLabels[] = "_services._dns-sd._udp"; Core::Core(Instance &aInstance) : InstanceLocator(aInstance) , mIsEnabled(false) , mIsQuestionUnicastAllowed(kDefaultQuAllowed) , mMaxMessageSize(kMaxMessageSize) , mInfraIfIndex(0) , mMultiPacketRxMessages(aInstance) , mNextProbeTxTime(TimerMilli::GetNow() - 1) , mEntryTimer(aInstance) , mEntryTask(aInstance) , mTxMessageHistory(aInstance) , mConflictCallback(nullptr) , mNextQueryTxTime(TimerMilli::GetNow() - 1) , mCacheTimer(aInstance) , mCacheTask(aInstance) { } Error Core::SetEnabled(bool aEnable, uint32_t aInfraIfIndex) { Error error = kErrorNone; VerifyOrExit(aEnable != mIsEnabled, error = kErrorAlready); SuccessOrExit(error = otPlatMdnsSetListeningEnabled(&GetInstance(), aEnable, aInfraIfIndex)); mIsEnabled = aEnable; mInfraIfIndex = aInfraIfIndex; if (mIsEnabled) { LogInfo("Enabling on infra-if-index %lu", ToUlong(mInfraIfIndex)); } else { LogInfo("Disabling"); } if (!mIsEnabled) { mHostEntries.Clear(); mServiceEntries.Clear(); mServiceTypes.Clear(); mMultiPacketRxMessages.Clear(); mTxMessageHistory.Clear(); mEntryTimer.Stop(); mBrowseCacheList.Clear(); mSrvCacheList.Clear(); mTxtCacheList.Clear(); mIp6AddrCacheList.Clear(); mIp4AddrCacheList.Clear(); mCacheTimer.Stop(); } Get().HandleMdnsCoreStateChange(); exit: return error; } #if OPENTHREAD_CONFIG_MULTICAST_DNS_AUTO_ENABLE_ON_INFRA_IF void Core::HandleInfraIfStateChanged(void) { IgnoreError(SetEnabled(Get().IsRunning(), Get().GetIfIndex())); } #endif template Error Core::Register(const ItemInfo &aItemInfo, RequestId aRequestId, RegisterCallback aCallback) { Error error = kErrorNone; EntryType *entry; VerifyOrExit(mIsEnabled, error = kErrorInvalidState); entry = GetEntryList().FindMatching(aItemInfo); if (entry == nullptr) { entry = EntryType::AllocateAndInit(GetInstance(), aItemInfo); OT_ASSERT(entry != nullptr); GetEntryList().Push(*entry); } entry->Register(aItemInfo, Callback(aRequestId, aCallback)); exit: return error; } template Error Core::Unregister(const ItemInfo &aItemInfo) { Error error = kErrorNone; EntryType *entry; VerifyOrExit(mIsEnabled, error = kErrorInvalidState); entry = GetEntryList().FindMatching(aItemInfo); if (entry != nullptr) { entry->Unregister(aItemInfo); } exit: return error; } Error Core::RegisterHost(const Host &aHost, RequestId aRequestId, RegisterCallback aCallback) { return Register(aHost, aRequestId, aCallback); } Error Core::UnregisterHost(const Host &aHost) { return Unregister(aHost); } Error Core::RegisterService(const Service &aService, RequestId aRequestId, RegisterCallback aCallback) { return Register(aService, aRequestId, aCallback); } Error Core::UnregisterService(const Service &aService) { return Unregister(aService); } Error Core::RegisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback) { return IsKeyForService(aKey) ? Register(aKey, aRequestId, aCallback) : Register(aKey, aRequestId, aCallback); } Error Core::UnregisterKey(const Key &aKey) { return IsKeyForService(aKey) ? Unregister(aKey) : Unregister(aKey); } #if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE Core::Iterator *Core::AllocateIterator(void) { return EntryIterator::Allocate(GetInstance()); } void Core::FreeIterator(Iterator &aIterator) { static_cast(aIterator).Free(); } Error Core::GetNextHost(Iterator &aIterator, Host &aHost, EntryState &aState) const { return static_cast(aIterator).GetNextHost(aHost, aState); } Error Core::GetNextService(Iterator &aIterator, Service &aService, EntryState &aState) const { return static_cast(aIterator).GetNextService(aService, aState); } Error Core::GetNextKey(Iterator &aIterator, Key &aKey, EntryState &aState) const { return static_cast(aIterator).GetNextKey(aKey, aState); } Error Core::GetNextBrowser(Iterator &aIterator, Browser &aBrowser, CacheInfo &aInfo) const { return static_cast(aIterator).GetNextBrowser(aBrowser, aInfo); } Error Core::GetNextSrvResolver(Iterator &aIterator, SrvResolver &aResolver, CacheInfo &aInfo) const { return static_cast(aIterator).GetNextSrvResolver(aResolver, aInfo); } Error Core::GetNextTxtResolver(Iterator &aIterator, TxtResolver &aResolver, CacheInfo &aInfo) const { return static_cast(aIterator).GetNextTxtResolver(aResolver, aInfo); } Error Core::GetNextIp6AddressResolver(Iterator &aIterator, AddressResolver &aResolver, CacheInfo &aInfo) const { return static_cast(aIterator).GetNextIp6AddressResolver(aResolver, aInfo); } Error Core::GetNextIp4AddressResolver(Iterator &aIterator, AddressResolver &aResolver, CacheInfo &aInfo) const { return static_cast(aIterator).GetNextIp4AddressResolver(aResolver, aInfo); } #endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE void Core::InvokeConflictCallback(const char *aName, const char *aServiceType) { if (mConflictCallback != nullptr) { mConflictCallback(&GetInstance(), aName, aServiceType); } } void Core::HandleMessage(Message &aMessage, bool aIsUnicast, const AddressInfo &aSenderAddress) { OwnedPtr messagePtr(&aMessage); OwnedPtr rxMessagePtr; VerifyOrExit(mIsEnabled); rxMessagePtr.Reset(RxMessage::AllocateAndInit(GetInstance(), messagePtr, aIsUnicast, aSenderAddress)); VerifyOrExit(!rxMessagePtr.IsNull()); if (rxMessagePtr->IsQuery()) { // Check if this is a continuation of a multi-packet query. // Initial query message sets the "Truncated" flag. // Subsequent messages from the same sender contain no // question and only known-answer records. if ((rxMessagePtr->GetRecordCounts().GetFor(kQuestionSection) == 0) && (rxMessagePtr->GetRecordCounts().GetFor(kAnswerSection) > 0)) { mMultiPacketRxMessages.AddToExisting(rxMessagePtr); ExitNow(); } switch (rxMessagePtr->ProcessQuery(/* aShouldProcessTruncated */ false)) { case RxMessage::kProcessed: break; case RxMessage::kSaveAsMultiPacket: // This is a truncated multi-packet query and we can // answer some questions in this query. We save it in // `mMultiPacketRxMessages` list and defer its response // for a random time waiting to receive next messages // containing additional known-answer records. mMultiPacketRxMessages.AddNew(rxMessagePtr); break; } } else { rxMessagePtr->ProcessResponse(); } exit: return; } void Core::HandleEntryTimer(void) { EntryContext context(GetInstance(), TxMessage::kMulticastResponse); NextFireTime nextAggrTxTime(context.GetNow()); // Determine the next multicast transmission time that is explicitly // after `GetNow()` to set `mNextAggrTxTime`. This is used for // response aggregation. As `HandleTimer()` is called on different // entries, they can decide to extend their answer delay to the // determined `mNextAggrTxTime` so that all answers are included in // the same response message. for (HostEntry &entry : mHostEntries) { entry.DetermineNextAggrTxTime(nextAggrTxTime); } for (ServiceEntry &entry : mServiceEntries) { entry.DetermineNextAggrTxTime(nextAggrTxTime); } for (ServiceType &serviceType : mServiceTypes) { serviceType.DetermineNextAggrTxTime(nextAggrTxTime); } context.mNextAggrTxTime = nextAggrTxTime.GetNextTime(); // We process host entries before service entries. This order // ensures we can determine whether host addresses have already // been appended to the Answer section (when processing service entries), // preventing duplicates. for (HostEntry &entry : mHostEntries) { entry.HandleTimer(context); } for (ServiceEntry &entry : mServiceEntries) { entry.HandleTimer(context); } for (ServiceType &serviceType : mServiceTypes) { serviceType.HandleTimer(context); } context.mProbeMessage.Send(); context.mResponseMessage.Send(); RemoveEmptyEntries(); mEntryTimer.FireAtIfEarlier(context.mNextFireTime); } void Core::RemoveEmptyEntries(void) { mHostEntries.RemoveAndFreeAllMatching(Entry::kRemoving); mServiceEntries.RemoveAndFreeAllMatching(Entry::kRemoving); } void Core::HandleEntryTask(void) { // `mEntryTask` serves two purposes: // // Invoking callbacks: This ensures `Register()` calls will always // return before invoking the callback, even when entry is // already in `kRegistered` state and registration is immediately // successful. // // Removing empty entries after `Unregister()` calls: This // prevents modification of `mHostEntries` and `mServiceEntries` // during callback execution while we are iterating over these // lists. Allows us to safely call `Register()` or `Unregister()` // from callbacks without iterator invalidation. for (HostEntry &entry : mHostEntries) { entry.InvokeCallbacks(); } for (ServiceEntry &entry : mServiceEntries) { entry.InvokeCallbacks(); } RemoveEmptyEntries(); } uint32_t Core::DetermineTtl(uint32_t aTtl, uint32_t aDefaultTtl) { return (aTtl == kUnspecifiedTtl) ? aDefaultTtl : aTtl; } bool Core::NameMatch(const Heap::String &aHeapString, const char *aName) { // Compares a DNS name given as a `Heap::String` with a // `aName` C string. return !aHeapString.IsNull() && StringMatch(aHeapString.AsCString(), aName, kStringCaseInsensitiveMatch); } bool Core::NameMatch(const Heap::String &aFirst, const Heap::String &aSecond) { // Compares two DNS names given as `Heap::String`. return !aSecond.IsNull() && NameMatch(aFirst, aSecond.AsCString()); } void Core::UpdateCacheFlushFlagIn(ResourceRecord &aResourceRecord, Section aSection, bool aIsLegacyUnicast) { // Do not set the cache-flush flag if the record is // appended in Authority Section in a probe message, // or is intended for a Legacy Unicast response. if (aSection != kAuthoritySection && !aIsLegacyUnicast) { aResourceRecord.SetClass(aResourceRecord.GetClass() | kClassCacheFlushFlag); } } void Core::UpdateRecordLengthInMessage(ResourceRecord &aRecord, Message &aMessage, uint16_t aOffset) { // Determines the records DATA length and updates it in a message. // Should be called immediately after all the fields in the // record are appended to the message. `aOffset` gives the offset // in the message to the start of the record. aRecord.SetLength(aMessage.GetLength() - aOffset - sizeof(ResourceRecord)); aMessage.Write(aOffset, aRecord); } void Core::UpdateCompressOffset(uint16_t &aOffset, uint16_t aNewOffset) { if ((aOffset == kUnspecifiedOffset) && (aNewOffset != kUnspecifiedOffset)) { aOffset = aNewOffset; } } bool Core::QuestionMatches(uint16_t aQuestionRrType, uint16_t aRrType) { return (aQuestionRrType == aRrType) || (aQuestionRrType == ResourceRecord::kTypeAny); } bool Core::RrClassIsInternetOrAny(uint16_t aRrClass) { aRrClass &= kClassMask; return (aRrClass == ResourceRecord::kClassInternet) || (aRrClass == ResourceRecord::kClassAny); } //---------------------------------------------------------------------------------------------------------------------- // Core::Callback Core::Callback::Callback(RequestId aRequestId, RegisterCallback aCallback) : mRequestId(aRequestId) , mCallback(aCallback) { } void Core::Callback::InvokeAndClear(Instance &aInstance, Error aError) { if (mCallback != nullptr) { RegisterCallback callback = mCallback; RequestId requestId = mRequestId; Clear(); callback(&aInstance, requestId, aError); } } //---------------------------------------------------------------------------------------------------------------------- // Core::RecordCounts void Core::RecordCounts::ReadFrom(const Header &aHeader) { mCounts[kQuestionSection] = aHeader.GetQuestionCount(); mCounts[kAnswerSection] = aHeader.GetAnswerCount(); mCounts[kAuthoritySection] = aHeader.GetAuthorityRecordCount(); mCounts[kAdditionalDataSection] = aHeader.GetAdditionalRecordCount(); } void Core::RecordCounts::WriteTo(Header &aHeader) const { aHeader.SetQuestionCount(mCounts[kQuestionSection]); aHeader.SetAnswerCount(mCounts[kAnswerSection]); aHeader.SetAuthorityRecordCount(mCounts[kAuthoritySection]); aHeader.SetAdditionalRecordCount(mCounts[kAdditionalDataSection]); } bool Core::RecordCounts::IsEmpty(void) const { // Indicates whether or not all counts are zero. bool isEmpty = true; for (uint16_t count : mCounts) { if (count != 0) { isEmpty = false; break; } } return isEmpty; } //---------------------------------------------------------------------------------------------------------------------- // Core::AddressArray bool Core::AddressArray::Matches(const Ip6::Address *aAddresses, uint16_t aNumAddresses) const { bool matches = false; VerifyOrExit(aNumAddresses == GetLength()); for (uint16_t i = 0; i < aNumAddresses; i++) { VerifyOrExit(Contains(aAddresses[i])); } matches = true; exit: return matches; } void Core::AddressArray::SetFrom(const Ip6::Address *aAddresses, uint16_t aNumAddresses) { Free(); SuccessOrAssert(ReserveCapacity(aNumAddresses)); for (uint16_t i = 0; i < aNumAddresses; i++) { IgnoreError(PushBack(aAddresses[i])); } } //---------------------------------------------------------------------------------------------------------------------- // Core::RecordInfo template void Core::RecordInfo::UpdateProperty(UintType &aProperty, UintType aValue) { // Updates a property variable associated with this record. The // `aProperty` is updated if the record is empty (has no value // yet) or if its current value differs from the new `aValue`. If // the property is changed, we prepare the record to be announced. // This template version works with `UintType` properties. There // are similar overloads for `Heap::Data` and `Heap::String` and // `AddressArray` property types below. static_assert(TypeTraits::IsSame::kValue || TypeTraits::IsSame::kValue || TypeTraits::IsSame::kValue || TypeTraits::IsSame::kValue, "UintType must be `uint8_t`, `uint16_t`, `uint32_t`, or `uint64_t`"); if (!mIsPresent || (aProperty != aValue)) { mIsPresent = true; aProperty = aValue; StartAnnouncing(); } } void Core::RecordInfo::UpdateProperty(Heap::String &aStringProperty, const char *aString) { if (!mIsPresent || !NameMatch(aStringProperty, aString)) { mIsPresent = true; SuccessOrAssert(aStringProperty.Set(aString)); StartAnnouncing(); } } void Core::RecordInfo::UpdateProperty(Heap::Data &aDataProperty, const uint8_t *aData, uint16_t aLength) { if (!mIsPresent || !aDataProperty.Matches(aData, aLength)) { mIsPresent = true; SuccessOrAssert(aDataProperty.SetFrom(aData, aLength)); StartAnnouncing(); } } void Core::RecordInfo::UpdateProperty(AddressArray &aAddrProperty, const Ip6::Address *aAddrs, uint16_t aNumAddrs) { if (!mIsPresent || !aAddrProperty.Matches(aAddrs, aNumAddrs)) { mIsPresent = true; aAddrProperty.SetFrom(aAddrs, aNumAddrs); StartAnnouncing(); } } uint32_t Core::RecordInfo::GetTtl(bool aIsLegacyUnicast) const { return aIsLegacyUnicast ? Min(kMaxLegacyUnicastTtl, mTtl) : mTtl; } void Core::RecordInfo::UpdateTtl(uint32_t aTtl) { return UpdateProperty(mTtl, aTtl); } void Core::RecordInfo::StartAnnouncing(void) { if (mIsPresent) { mAnnounceCounter = 0; mAnnounceTime = TimerMilli::GetNow(); } } bool Core::RecordInfo::CanAnswer(void) const { return (mIsPresent && (mTtl > 0)); } void Core::RecordInfo::ScheduleAnswer(const AnswerInfo &aInfo) { VerifyOrExit(CanAnswer()); if (aInfo.mUnicastResponse || aInfo.mLegacyUnicastResponse) { mUnicastAnswerPending = true; ExitNow(); } if (!aInfo.mIsProbe) { // Rate-limiting multicasts to prevent excessive packet flooding // (RFC 6762 section 6): We enforce a minimum interval of one // second (`kMinIntervalBetweenMulticast`) between multicast // transmissions of the same record. Skip the new request if the // answer time is too close to the last multicast time. A querier // that did not receive and cache the previous transmission will // retry its request. VerifyOrExit(GetDurationSinceLastMulticast(aInfo.GetAnswerTime()) >= kMinIntervalBetweenMulticast); } if (mMulticastAnswerPending) { TimeMilli targetAnswerTime; if (mCanExtendAnswerDelay && aInfo.mIsProbe) { mCanExtendAnswerDelay = false; } targetAnswerTime = Min(aInfo.GetAnswerTime(), GetAnswerTime()); mQueryRxTime = Min(aInfo.mQueryRxTime, mQueryRxTime); mAnswerDelay = targetAnswerTime - mQueryRxTime; } else { mMulticastAnswerPending = true; mCanExtendAnswerDelay = !aInfo.mIsProbe; mQueryRxTime = aInfo.mQueryRxTime; mAnswerDelay = aInfo.mAnswerDelay; } exit: return; } bool Core::RecordInfo::ShouldAppendTo(EntryContext &aContext) { bool shouldAppend = false; VerifyOrExit(mIsPresent); switch (aContext.mResponseMessage.GetType()) { case TxMessage::kMulticastResponse: if ((mAnnounceCounter < kNumberOfAnnounces) && (mAnnounceTime <= aContext.GetNow())) { shouldAppend = true; ExitNow(); } if (mMulticastAnswerPending && (GetAnswerTime() <= aContext.GetNow())) { // Check if we can delay the answer further so that it can // be aggregated with other responses scheduled to go out a // little later. if (ExtendAnswerDelay(aContext) == kErrorNone) { ExitNow(); } shouldAppend = true; } break; case TxMessage::kUnicastResponse: case TxMessage::kLegacyUnicastResponse: shouldAppend = mUnicastAnswerPending; break; default: break; } exit: return shouldAppend; } Error Core::RecordInfo::ExtendAnswerDelay(EntryContext &aContext) { Error error = kErrorFailed; // Extend the answer delay for response aggregation when possible. // // This method is called when we have a pending multicast answer // (`mMulticastAnswerPending`) and the answer time has already // expired. We first check if the answer can be delayed (e.g., it // is not allowed for probe responses) and that there is an // upcoming `mNextAggrTxTime` within a short window of time from // `GetNow()`, before extending the delay. We ensure that the // overall answer delay does not exceed // `kResponseAggregationMaxDelay`. VerifyOrExit(mCanExtendAnswerDelay); VerifyOrExit(aContext.mNextAggrTxTime != aContext.GetNow().GetDistantFuture()); VerifyOrExit(aContext.mNextAggrTxTime - aContext.GetNow() < kResponseAggregationMaxDelay); VerifyOrExit(aContext.mNextAggrTxTime - mQueryRxTime < kResponseAggregationMaxDelay); mAnswerDelay = aContext.mNextAggrTxTime - mQueryRxTime; error = kErrorNone; exit: return error; } void Core::RecordInfo::UpdateStateAfterAnswer(const TxMessage &aResponse) { // Updates the state after a unicast or multicast response is // prepared containing the record in the Answer section. VerifyOrExit(mIsPresent); switch (aResponse.GetType()) { case TxMessage::kMulticastResponse: VerifyOrExit(mAppendState == kAppendedInMulticastMsg); VerifyOrExit(mAppendSection == kAnswerSection); mMulticastAnswerPending = false; if (mAnnounceCounter < kNumberOfAnnounces) { mAnnounceCounter++; if (mAnnounceCounter < kNumberOfAnnounces) { uint32_t delay = (1U << (mAnnounceCounter - 1)) * kAnnounceInterval; mAnnounceTime = TimerMilli::GetNow() + delay; } else if (mTtl == 0) { // We are done announcing the removed record with zero TTL. mIsPresent = false; } } break; case TxMessage::kUnicastResponse: case TxMessage::kLegacyUnicastResponse: VerifyOrExit(IsAppended()); VerifyOrExit(mAppendSection == kAnswerSection); mUnicastAnswerPending = false; break; default: break; } exit: return; } void Core::RecordInfo::UpdateFireTimeOn(FireTime &aFireTime) { VerifyOrExit(mIsPresent); if (mAnnounceCounter < kNumberOfAnnounces) { aFireTime.SetFireTime(mAnnounceTime); } if (mMulticastAnswerPending) { aFireTime.SetFireTime(GetAnswerTime()); } if (mIsLastMulticastValid) { // `mLastMulticastTime` tracks the timestamp of the last // multicast of this record. To handle potential 32-bit // `TimeMilli` rollover, an aging mechanism is implemented. // If the record isn't multicast again within a given age // interval `kLastMulticastTimeAge`, `mIsLastMulticastValid` // is cleared, indicating outdated multicast information. TimeMilli lastMulticastAgeTime = mLastMulticastTime + kLastMulticastTimeAge; if (lastMulticastAgeTime <= TimerMilli::GetNow()) { mIsLastMulticastValid = false; } else { aFireTime.SetFireTime(lastMulticastAgeTime); } } exit: return; } void Core::RecordInfo::DetermineNextAggrTxTime(NextFireTime &aNextAggrTxTime) const { VerifyOrExit(mIsPresent); if (mAnnounceCounter < kNumberOfAnnounces) { aNextAggrTxTime.UpdateIfEarlierAndInFuture(mAnnounceTime); } if (mMulticastAnswerPending) { aNextAggrTxTime.UpdateIfEarlierAndInFuture(GetAnswerTime()); } exit: return; } void Core::RecordInfo::MarkAsAppended(TxMessage &aTxMessage, Section aSection) { mAppendSection = aSection; switch (aTxMessage.GetType()) { case TxMessage::kMulticastResponse: case TxMessage::kMulticastProbe: mAppendState = kAppendedInMulticastMsg; if ((aSection == kAnswerSection) || (aSection == kAdditionalDataSection)) { mLastMulticastTime = TimerMilli::GetNow(); mIsLastMulticastValid = true; } break; case TxMessage::kUnicastResponse: case TxMessage::kLegacyUnicastResponse: mAppendState = kAppendedInUnicastMsg; break; case TxMessage::kMulticastQuery: break; } } void Core::RecordInfo::MarkToAppendInAdditionalData(void) { if (mAppendState == kNotAppended) { mAppendState = kToAppendInAdditionalData; } } bool Core::RecordInfo::IsAppended(void) const { bool isAppended = false; switch (mAppendState) { case kNotAppended: case kToAppendInAdditionalData: break; case kAppendedInMulticastMsg: case kAppendedInUnicastMsg: isAppended = true; break; } return isAppended; } bool Core::RecordInfo::CanAppend(void) const { return mIsPresent && !IsAppended(); } Error Core::RecordInfo::GetLastMulticastTime(TimeMilli &aLastMulticastTime) const { Error error = kErrorNotFound; VerifyOrExit(mIsPresent && mIsLastMulticastValid); aLastMulticastTime = mLastMulticastTime; exit: return error; } uint32_t Core::RecordInfo::GetDurationSinceLastMulticast(TimeMilli aTime) const { uint32_t duration = NumericLimits::kMax; VerifyOrExit(mIsPresent && mIsLastMulticastValid); VerifyOrExit(aTime > mLastMulticastTime, duration = 0); duration = aTime - mLastMulticastTime; exit: return duration; } //---------------------------------------------------------------------------------------------------------------------- // Core::FireTime void Core::FireTime::SetFireTime(TimeMilli aFireTime) { if (mHasFireTime) { VerifyOrExit(aFireTime < mFireTime); } mFireTime = aFireTime; mHasFireTime = true; exit: return; } void Core::FireTime::ScheduleFireTimeOn(TimerMilli &aTimer) { if (mHasFireTime) { aTimer.FireAtIfEarlier(mFireTime); } } void Core::FireTime::UpdateNextFireTimeOn(NextFireTime &aNextFireTime) const { if (mHasFireTime) { aNextFireTime.UpdateIfEarlier(mFireTime); } } //---------------------------------------------------------------------------------------------------------------------- // Core::Entry Core::Entry::Entry(void) : mState(kProbing) , mProbeCount(0) , mMulticastNsecPending(false) , mUnicastNsecPending(false) , mAppendedNsec(false) , mBypassCallbackStateCheck(false) { } void Core::Entry::Init(Instance &aInstance) { // Initializes a newly allocated entry (host or service) // and starts it in `kProbing` state. InstanceLocatorInit::Init(aInstance); StartProbing(); } void Core::Entry::SetState(State aState) { mState = aState; ScheduleCallbackTask(); } void Core::Entry::Register(const Key &aKey, const Callback &aCallback) { if (GetState() == kRemoving) { StartProbing(); } mKeyRecord.UpdateTtl(DetermineTtl(aKey.mTtl, kDefaultKeyTtl)); mKeyRecord.UpdateProperty(mKeyData, aKey.mKeyData, aKey.mKeyDataLength); mKeyCallback = aCallback; ScheduleCallbackTask(); } void Core::Entry::Unregister(const Key &aKey) { OT_UNUSED_VARIABLE(aKey); VerifyOrExit(mKeyRecord.IsPresent()); mKeyCallback.Clear(); switch (GetState()) { case kRegistered: mKeyRecord.UpdateTtl(0); break; case kProbing: case kConflict: ClearKey(); break; case kRemoving: break; } exit: return; } void Core::Entry::ClearKey(void) { mKeyRecord.Clear(); mKeyData.Free(); } void Core::Entry::SetCallback(const Callback &aCallback) { mCallback = aCallback; ScheduleCallbackTask(); } void Core::Entry::MarkToInvokeCallbackUnconditionally(void) { mBypassCallbackStateCheck = true; Get().mEntryTask.Post(); } void Core::Entry::ScheduleCallbackTask(void) { switch (GetState()) { case kRegistered: case kConflict: VerifyOrExit(!mCallback.IsEmpty() || !mKeyCallback.IsEmpty()); Get().mEntryTask.Post(); break; case kProbing: case kRemoving: break; } exit: return; } void Core::Entry::InvokeCallbacks(void) { Error error = kErrorNone; // `mBypassCallbackStateCheck` is used when host is registered // with no address, which is treated as unregistering the host. // This ensures host registration callback is invoked properly. if (mBypassCallbackStateCheck) { mBypassCallbackStateCheck = false; mCallback.InvokeAndClear(GetInstance(), error); } switch (GetState()) { case kConflict: error = kErrorDuplicated; OT_FALL_THROUGH; case kRegistered: mKeyCallback.InvokeAndClear(GetInstance(), error); mCallback.InvokeAndClear(GetInstance(), error); break; case kProbing: case kRemoving: break; } } void Core::Entry::StartProbing(void) { SetState(kProbing); mProbeCount = 0; SetFireTime(Get().RandomizeFirstProbeTxTime()); ScheduleTimer(); } void Core::Entry::SetStateToConflict(void) { switch (GetState()) { case kProbing: case kRegistered: SetState(kConflict); break; case kConflict: case kRemoving: break; } } void Core::Entry::SetStateToRemoving(void) { VerifyOrExit(GetState() != kRemoving); SetState(kRemoving); exit: return; } void Core::Entry::ClearAppendState(void) { mKeyRecord.MarkAsNotAppended(); mAppendedNsec = false; } void Core::Entry::UpdateRecordsState(const TxMessage &aResponse) { mKeyRecord.UpdateStateAfterAnswer(aResponse); if (mAppendedNsec) { switch (aResponse.GetType()) { case TxMessage::kMulticastResponse: mMulticastNsecPending = false; break; case TxMessage::kUnicastResponse: mUnicastNsecPending = false; break; default: break; } } } void Core::Entry::ScheduleNsecAnswer(const AnswerInfo &aInfo) { // Schedules NSEC record to be included in a response message. // Used to answer to query for a record that is not present. VerifyOrExit(GetState() == kRegistered); if (aInfo.mUnicastResponse) { mUnicastNsecPending = true; } else { if (mMulticastNsecPending) { TimeMilli targetAnswerTime = Min(aInfo.GetAnswerTime(), GetNsecAnswerTime()); mNsecQueryRxTime = Min(aInfo.mQueryRxTime, mNsecQueryRxTime); mNsecAnswerDelay = targetAnswerTime - mNsecQueryRxTime; } else { mMulticastNsecPending = true; mNsecQueryRxTime = aInfo.mQueryRxTime; mNsecAnswerDelay = aInfo.mAnswerDelay; } } exit: return; } bool Core::Entry::ShouldAnswerNsec(TimeMilli aNow) const { return mMulticastNsecPending && (GetNsecAnswerTime() <= aNow); } void Core::Entry::AnswerNonProbe(const AnswerInfo &aInfo, RecordAndType *aRecords, uint16_t aRecordsLength) { // Schedule answers for all matching records in `aRecords` array // to a given non-probe question. bool allEmptyOrZeroTtl = true; bool answerNsec = true; for (uint16_t index = 0; index < aRecordsLength; index++) { RecordInfo &record = aRecords[index].mRecord; if (!record.CanAnswer()) { // Cannot answer if record is not present or has zero TTL. continue; } allEmptyOrZeroTtl = false; if (QuestionMatches(aInfo.mQuestionRrType, aRecords[index].mType)) { answerNsec = false; record.ScheduleAnswer(aInfo); } } // If all records are removed or have zero TTL (we are still // sending "Goodbye" announces), we should not provide any answer // even NSEC. if (!allEmptyOrZeroTtl && answerNsec) { ScheduleNsecAnswer(aInfo); } } void Core::Entry::AnswerProbe(const AnswerInfo &aInfo, RecordAndType *aRecords, uint16_t aRecordsLength) { bool allEmptyOrZeroTtl = true; bool shouldDelay = false; TimeMilli now = TimerMilli::GetNow(); AnswerInfo info = aInfo; info.mAnswerDelay = 0; OT_ASSERT(info.mIsProbe); for (uint16_t index = 0; index < aRecordsLength; index++) { RecordInfo &record = aRecords[index].mRecord; TimeMilli lastMulticastTime; if (!record.CanAnswer()) { continue; } allEmptyOrZeroTtl = false; if (!info.mUnicastResponse) { // Rate limiting multicast probe responses // // We delay the response if all records were multicast // recently within an interval `kMinIntervalProbeResponse` // (250 msec). if (record.GetDurationSinceLastMulticast(now) >= kMinIntervalProbeResponse) { shouldDelay = false; } else if (record.GetLastMulticastTime(lastMulticastTime) == kErrorNone) { info.mAnswerDelay = Max(info.GetAnswerTime(), lastMulticastTime + kMinIntervalProbeResponse) - info.mQueryRxTime; } } } if (allEmptyOrZeroTtl) { // All records are removed or being removed. // Enhancement for future: If someone is probing for // our name, we can stop announcement of removed records // to let the new probe requester take over the name. ExitNow(); } if (!shouldDelay) { info.mAnswerDelay = 0; } for (uint16_t index = 0; index < aRecordsLength; index++) { aRecords[index].mRecord.ScheduleAnswer(info); } exit: return; } void Core::Entry::DetermineNextFireTime(void) { mKeyRecord.UpdateFireTimeOn(*this); if (mMulticastNsecPending) { SetFireTime(GetNsecAnswerTime()); } } void Core::Entry::DetermineNextAggrTxTime(NextFireTime &aNextAggrTxTime) const { mKeyRecord.DetermineNextAggrTxTime(aNextAggrTxTime); if (mMulticastNsecPending) { aNextAggrTxTime.UpdateIfEarlierAndInFuture(GetNsecAnswerTime()); } } void Core::Entry::ScheduleTimer(void) { ScheduleFireTimeOn(Get().mEntryTimer); } template void Core::Entry::HandleTimer(EntryContext &aContext) { EntryType *thisAsEntryType = static_cast(this); thisAsEntryType->ClearAppendState(); VerifyOrExit(HasFireTime()); VerifyOrExit(GetFireTime() <= aContext.GetNow()); ClearFireTime(); switch (GetState()) { case kProbing: if (mProbeCount < kNumberOfProbes) { mProbeCount++; SetFireTime(aContext.GetNow() + kProbeWaitTime); thisAsEntryType->PrepareProbe(aContext.mProbeMessage); break; } SetState(kRegistered); thisAsEntryType->StartAnnouncing(); OT_FALL_THROUGH; case kRegistered: thisAsEntryType->PrepareResponse(aContext); break; case kConflict: case kRemoving: ExitNow(); } thisAsEntryType->DetermineNextFireTime(); exit: UpdateNextFireTimeOn(aContext.mNextFireTime); } void Core::Entry::AppendQuestionTo(TxMessage &aTxMessage) const { Message &message = aTxMessage.SelectMessageFor(kQuestionSection); uint16_t rrClass = ResourceRecord::kClassInternet; Question question; if ((mProbeCount == 1) && Get().IsQuestionUnicastAllowed()) { rrClass |= kClassQuestionUnicastFlag; } question.SetType(ResourceRecord::kTypeAny); question.SetClass(rrClass); SuccessOrAssert(message.Append(question)); aTxMessage.IncrementRecordCount(kQuestionSection); } void Core::Entry::AppendKeyRecordTo(TxMessage &aTxMessage, Section aSection, NameAppender aNameAppender) { Message *message; ResourceRecord record; bool isLegacyUnicast = (aTxMessage.GetType() == TxMessage::kLegacyUnicastResponse); VerifyOrExit(mKeyRecord.CanAppend()); mKeyRecord.MarkAsAppended(aTxMessage, aSection); message = &aTxMessage.SelectMessageFor(aSection); // Use the `aNameAppender` function to allow sub-class // to append the proper name. aNameAppender(*this, aTxMessage, aSection); record.Init(ResourceRecord::kTypeKey); record.SetLength(mKeyData.GetLength()); record.SetTtl(mKeyRecord.GetTtl(isLegacyUnicast)); UpdateCacheFlushFlagIn(record, aSection, isLegacyUnicast); SuccessOrAssert(message->Append(record)); SuccessOrAssert(message->AppendBytes(mKeyData.GetBytes(), mKeyData.GetLength())); aTxMessage.IncrementRecordCount(aSection); exit: return; } void Core::Entry::AppendNsecRecordTo(TxMessage &aTxMessage, Section aSection, const TypeArray &aTypes, NameAppender aNameAppender) { Message &message = aTxMessage.SelectMessageFor(aSection); NsecRecord nsec; NsecRecord::TypeBitMap bitmap; uint16_t offset; bool isLegacyUnicast = (aTxMessage.GetType() == TxMessage::kLegacyUnicastResponse); nsec.Init(); nsec.SetTtl(isLegacyUnicast ? kLegacyUnicastNsecTtl : kNsecTtl); UpdateCacheFlushFlagIn(nsec, aSection, isLegacyUnicast); bitmap.Clear(); for (uint16_t type : aTypes) { bitmap.AddType(type); } aNameAppender(*this, aTxMessage, aSection); offset = message.GetLength(); SuccessOrAssert(message.Append(nsec)); // Next Domain Name (should be same as record name). aNameAppender(*this, aTxMessage, aSection); SuccessOrAssert(message.AppendBytes(&bitmap, bitmap.GetSize())); UpdateRecordLengthInMessage(nsec, message, offset); aTxMessage.IncrementRecordCount(aSection); mAppendedNsec = true; } Error Core::Entry::CopyKeyInfoTo(Key &aKey, EntryState &aState) const { Error error = kErrorNone; VerifyOrExit(mKeyRecord.IsPresent(), error = kErrorNotFound); aKey.mKeyData = mKeyData.GetBytes(); aKey.mKeyDataLength = mKeyData.GetLength(); aKey.mClass = ResourceRecord::kClassInternet; aKey.mTtl = mKeyRecord.GetTtl(); aKey.mInfraIfIndex = Get().mInfraIfIndex; aState = static_cast(GetState()); exit: return error; } //---------------------------------------------------------------------------------------------------------------------- // Core::HostEntry Core::HostEntry::HostEntry(void) : mNext(nullptr) , mNameOffset(kUnspecifiedOffset) { } Error Core::HostEntry::Init(Instance &aInstance, const char *aName) { Entry::Init(aInstance); return mName.Set(aName); } bool Core::HostEntry::Matches(const Name &aName) const { return aName.Matches(/* aFirstLabel */ nullptr, mName.AsCString(), kLocalDomain); } bool Core::HostEntry::Matches(const Host &aHost) const { return NameMatch(mName, aHost.mHostName); } bool Core::HostEntry::Matches(const Key &aKey) const { return !IsKeyForService(aKey) && NameMatch(mName, aKey.mName); } bool Core::HostEntry::Matches(const Heap::String &aName) const { return NameMatch(mName, aName); } bool Core::HostEntry::IsEmpty(void) const { return !mAddrRecord.IsPresent() && !mKeyRecord.IsPresent(); } void Core::HostEntry::Register(const Host &aHost, const Callback &aCallback) { if (GetState() == kRemoving) { StartProbing(); } SetCallback(aCallback); if (aHost.mAddressesLength == 0) { // If host is registered with no addresses, treat it // as host being unregistered and announce removal of // the old addresses. Unregister(aHost); // Set the callback again as `Unregister()` may clear it. // Also mark to invoke the callback unconditionally (bypassing // entry state check). The callback will be invoked // after returning from this method from the posted tasklet. SetCallback(aCallback); MarkToInvokeCallbackUnconditionally(); ExitNow(); } mAddrRecord.UpdateTtl(DetermineTtl(aHost.mTtl, kDefaultTtl)); mAddrRecord.UpdateProperty(mAddresses, AsCoreTypePtr(aHost.mAddresses), aHost.mAddressesLength); DetermineNextFireTime(); ScheduleTimer(); exit: return; } void Core::HostEntry::Register(const Key &aKey, const Callback &aCallback) { Entry::Register(aKey, aCallback); DetermineNextFireTime(); ScheduleTimer(); } void Core::HostEntry::Unregister(const Host &aHost) { OT_UNUSED_VARIABLE(aHost); VerifyOrExit(mAddrRecord.IsPresent()); ClearCallback(); switch (GetState()) { case kRegistered: mAddrRecord.UpdateTtl(0); DetermineNextFireTime(); ScheduleTimer(); break; case kProbing: case kConflict: ClearHost(); ScheduleToRemoveIfEmpty(); break; case kRemoving: break; } exit: return; } void Core::HostEntry::Unregister(const Key &aKey) { Entry::Unregister(aKey); DetermineNextFireTime(); ScheduleTimer(); ScheduleToRemoveIfEmpty(); } void Core::HostEntry::ClearHost(void) { mAddrRecord.Clear(); mAddresses.Free(); } void Core::HostEntry::ScheduleToRemoveIfEmpty(void) { if (IsEmpty()) { SetStateToRemoving(); Get().mEntryTask.Post(); } } void Core::HostEntry::HandleConflict(void) { State oldState = GetState(); SetStateToConflict(); VerifyOrExit(oldState == kRegistered); Get().InvokeConflictCallback(mName.AsCString(), nullptr); exit: return; } void Core::HostEntry::AnswerQuestion(const AnswerInfo &aInfo) { RecordAndType records[] = { {mAddrRecord, ResourceRecord::kTypeAaaa}, {mKeyRecord, ResourceRecord::kTypeKey}, }; VerifyOrExit(GetState() == kRegistered); if (aInfo.mIsProbe) { AnswerProbe(aInfo, records, GetArrayLength(records)); } else { AnswerNonProbe(aInfo, records, GetArrayLength(records)); } DetermineNextFireTime(); ScheduleTimer(); exit: return; } void Core::HostEntry::HandleTimer(EntryContext &aContext) { Entry::HandleTimer(aContext); } void Core::HostEntry::ClearAppendState(void) { // Clears `HostEntry` records and all tracked saved name // compression offsets. Entry::ClearAppendState(); mAddrRecord.MarkAsNotAppended(); mNameOffset = kUnspecifiedOffset; } void Core::HostEntry::PrepareProbe(TxMessage &aProbe) { bool prepareAgain = false; do { aProbe.SaveCurrentState(); AppendNameTo(aProbe, kQuestionSection); AppendQuestionTo(aProbe); AppendAddressRecordsTo(aProbe, kAuthoritySection); AppendKeyRecordTo(aProbe, kAuthoritySection); aProbe.CheckSizeLimitToPrepareAgain(prepareAgain); } while (prepareAgain); } void Core::HostEntry::StartAnnouncing(void) { mAddrRecord.StartAnnouncing(); mKeyRecord.StartAnnouncing(); } void Core::HostEntry::PrepareResponse(EntryContext &aContext) { bool prepareAgain = false; TxMessage &response = aContext.mResponseMessage; do { response.SaveCurrentState(); PrepareResponseRecords(aContext); response.CheckSizeLimitToPrepareAgain(prepareAgain); } while (prepareAgain); UpdateRecordsState(response); } void Core::HostEntry::PrepareResponseRecords(EntryContext &aContext) { bool appendNsec = false; TxMessage &response = aContext.mResponseMessage; if (mAddrRecord.ShouldAppendTo(aContext)) { AppendAddressRecordsTo(response, kAnswerSection); appendNsec = true; } if (mKeyRecord.ShouldAppendTo(aContext)) { AppendKeyRecordTo(response, kAnswerSection); appendNsec = true; } if (appendNsec || ShouldAnswerNsec(aContext.GetNow())) { AppendNsecRecordTo(response, kAdditionalDataSection); } } void Core::HostEntry::UpdateRecordsState(const TxMessage &aResponse) { // Updates state after a response is prepared. Entry::UpdateRecordsState(aResponse); mAddrRecord.UpdateStateAfterAnswer(aResponse); if (IsEmpty()) { SetStateToRemoving(); } } void Core::HostEntry::DetermineNextFireTime(void) { VerifyOrExit(GetState() == kRegistered); Entry::DetermineNextFireTime(); mAddrRecord.UpdateFireTimeOn(*this); exit: return; } void Core::HostEntry::DetermineNextAggrTxTime(NextFireTime &aNextAggrTxTime) const { VerifyOrExit(GetState() == kRegistered); Entry::DetermineNextAggrTxTime(aNextAggrTxTime); mAddrRecord.DetermineNextAggrTxTime(aNextAggrTxTime); exit: return; } void Core::HostEntry::AppendAddressRecordsTo(TxMessage &aTxMessage, Section aSection) { Message *message; bool isLegacyUnicast = (aTxMessage.GetType() == TxMessage::kLegacyUnicastResponse); VerifyOrExit(mAddrRecord.CanAppend()); mAddrRecord.MarkAsAppended(aTxMessage, aSection); message = &aTxMessage.SelectMessageFor(aSection); for (const Ip6::Address &address : mAddresses) { AaaaRecord aaaaRecord; aaaaRecord.Init(); aaaaRecord.SetAddress(address); aaaaRecord.SetTtl(mAddrRecord.GetTtl(isLegacyUnicast)); UpdateCacheFlushFlagIn(aaaaRecord, aSection, isLegacyUnicast); AppendNameTo(aTxMessage, aSection); SuccessOrAssert(message->Append(aaaaRecord)); aTxMessage.IncrementRecordCount(aSection); } exit: return; } void Core::HostEntry::AppendKeyRecordTo(TxMessage &aTxMessage, Section aSection) { Entry::AppendKeyRecordTo(aTxMessage, aSection, &AppendEntryName); } void Core::HostEntry::AppendNsecRecordTo(TxMessage &aTxMessage, Section aSection) { TypeArray types; if (mAddrRecord.IsPresent() && (mAddrRecord.GetTtl() > 0)) { types.Add(ResourceRecord::kTypeAaaa); } if (mKeyRecord.IsPresent() && (mKeyRecord.GetTtl() > 0)) { types.Add(ResourceRecord::kTypeKey); } if (!types.IsEmpty()) { Entry::AppendNsecRecordTo(aTxMessage, aSection, types, &AppendEntryName); } } void Core::HostEntry::AppendEntryName(Entry &aEntry, TxMessage &aTxMessage, Section aSection) { static_cast(aEntry).AppendNameTo(aTxMessage, aSection); } void Core::HostEntry::AppendNameTo(TxMessage &aTxMessage, Section aSection) { AppendOutcome outcome; outcome = aTxMessage.AppendMultipleLabels(aSection, mName.AsCString(), mNameOffset); VerifyOrExit(outcome != kAppendedFullNameAsCompressed); aTxMessage.AppendDomainName(aSection); exit: return; } #if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE Error Core::HostEntry::CopyInfoTo(Host &aHost, EntryState &aState) const { Error error = kErrorNone; VerifyOrExit(mAddrRecord.IsPresent(), error = kErrorNotFound); aHost.mHostName = mName.AsCString(); aHost.mAddresses = mAddresses.AsCArray(); aHost.mAddressesLength = mAddresses.GetLength(); aHost.mTtl = mAddrRecord.GetTtl(); aHost.mInfraIfIndex = Get().mInfraIfIndex; aState = static_cast(GetState()); exit: return error; } Error Core::HostEntry::CopyInfoTo(Key &aKey, EntryState &aState) const { Error error; SuccessOrExit(error = CopyKeyInfoTo(aKey, aState)); aKey.mName = mName.AsCString(); aKey.mServiceType = nullptr; exit: return error; } #endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE //---------------------------------------------------------------------------------------------------------------------- // Core::ServiceEntry const uint8_t Core::ServiceEntry::kEmptyTxtData[] = {0}; Core::ServiceEntry::ServiceEntry(void) : mNext(nullptr) , mPriority(0) , mWeight(0) , mPort(0) , mServiceNameOffset(kUnspecifiedOffset) , mServiceTypeOffset(kUnspecifiedOffset) , mSubServiceTypeOffset(kUnspecifiedOffset) , mHostNameOffset(kUnspecifiedOffset) , mIsAddedInServiceTypes(false) { } Error Core::ServiceEntry::Init(Instance &aInstance, const char *aServiceInstance, const char *aServiceType) { Error error; Entry::Init(aInstance); SuccessOrExit(error = mServiceInstance.Set(aServiceInstance)); SuccessOrExit(error = mServiceType.Set(aServiceType)); exit: return error; } Error Core::ServiceEntry::Init(Instance &aInstance, const Service &aService) { return Init(aInstance, aService.mServiceInstance, aService.mServiceType); } Error Core::ServiceEntry::Init(Instance &aInstance, const Key &aKey) { return Init(aInstance, aKey.mName, aKey.mServiceType); } bool Core::ServiceEntry::Matches(const Name &aFullName) const { return aFullName.Matches(mServiceInstance.AsCString(), mServiceType.AsCString(), kLocalDomain); } bool Core::ServiceEntry::MatchesServiceType(const Name &aServiceType) const { // When matching service type, PTR record should be // present with non-zero TTL (checked by `CanAnswer()`). return mPtrRecord.CanAnswer() && aServiceType.Matches(nullptr, mServiceType.AsCString(), kLocalDomain); } bool Core::ServiceEntry::Matches(const Service &aService) const { return NameMatch(mServiceInstance, aService.mServiceInstance) && NameMatch(mServiceType, aService.mServiceType); } bool Core::ServiceEntry::Matches(const Key &aKey) const { return IsKeyForService(aKey) && NameMatch(mServiceInstance, aKey.mName) && NameMatch(mServiceType, aKey.mServiceType); } bool Core::ServiceEntry::IsEmpty(void) const { return !mPtrRecord.IsPresent() && !mKeyRecord.IsPresent(); } bool Core::ServiceEntry::CanAnswerSubType(const char *aSubLabel) const { bool canAnswer = false; const SubType *subType; VerifyOrExit(mPtrRecord.CanAnswer()); subType = mSubTypes.FindMatching(aSubLabel); VerifyOrExit(subType != nullptr); canAnswer = subType->mPtrRecord.CanAnswer(); exit: return canAnswer; } void Core::ServiceEntry::Register(const Service &aService, const Callback &aCallback) { uint32_t ttl = DetermineTtl(aService.mTtl, kDefaultTtl); if (GetState() == kRemoving) { StartProbing(); } SetCallback(aCallback); // Register sub-types PTRs. // First we check for any removed sub-types. We keep removed // sub-types marked with zero TTL so to announce their removal // before fully removing them from the list. for (SubType &subType : mSubTypes) { uint32_t subTypeTtl = subType.IsContainedIn(aService) ? ttl : 0; subType.mPtrRecord.UpdateTtl(subTypeTtl); } // Next we add any new sub-types in `aService`. for (uint16_t i = 0; i < aService.mSubTypeLabelsLength; i++) { const char *label = aService.mSubTypeLabels[i]; if (!mSubTypes.ContainsMatching(label)) { SubType *newSubType = SubType::AllocateAndInit(label); OT_ASSERT(newSubType != nullptr); mSubTypes.Push(*newSubType); newSubType->mPtrRecord.UpdateTtl(ttl); } } // Register base PTR service. mPtrRecord.UpdateTtl(ttl); // Register SRV record info. mSrvRecord.UpdateTtl(ttl); mSrvRecord.UpdateProperty(mHostName, aService.mHostName); mSrvRecord.UpdateProperty(mPriority, aService.mPriority); mSrvRecord.UpdateProperty(mWeight, aService.mWeight); mSrvRecord.UpdateProperty(mPort, aService.mPort); // Register TXT record info. mTxtRecord.UpdateTtl(ttl); if ((aService.mTxtData == nullptr) || (aService.mTxtDataLength == 0)) { mTxtRecord.UpdateProperty(mTxtData, kEmptyTxtData, sizeof(kEmptyTxtData)); } else { mTxtRecord.UpdateProperty(mTxtData, aService.mTxtData, aService.mTxtDataLength); } UpdateServiceTypes(); DetermineNextFireTime(); ScheduleTimer(); } void Core::ServiceEntry::Register(const Key &aKey, const Callback &aCallback) { Entry::Register(aKey, aCallback); DetermineNextFireTime(); ScheduleTimer(); } void Core::ServiceEntry::Unregister(const Service &aService) { OT_UNUSED_VARIABLE(aService); VerifyOrExit(mPtrRecord.IsPresent()); ClearCallback(); switch (GetState()) { case kRegistered: for (SubType &subType : mSubTypes) { subType.mPtrRecord.UpdateTtl(0); } mPtrRecord.UpdateTtl(0); mSrvRecord.UpdateTtl(0); mTxtRecord.UpdateTtl(0); DetermineNextFireTime(); ScheduleTimer(); break; case kProbing: case kConflict: ClearService(); ScheduleToRemoveIfEmpty(); break; case kRemoving: break; } UpdateServiceTypes(); exit: return; } void Core::ServiceEntry::Unregister(const Key &aKey) { Entry::Unregister(aKey); DetermineNextFireTime(); ScheduleTimer(); ScheduleToRemoveIfEmpty(); } void Core::ServiceEntry::ClearService(void) { mPtrRecord.Clear(); mSrvRecord.Clear(); mTxtRecord.Clear(); mSubTypes.Free(); mHostName.Free(); mTxtData.Free(); } void Core::ServiceEntry::ScheduleToRemoveIfEmpty(void) { mSubTypes.RemoveAndFreeAllMatching(EmptyChecker()); if (IsEmpty()) { SetStateToRemoving(); Get().mEntryTask.Post(); } } void Core::ServiceEntry::HandleConflict(void) { State oldState = GetState(); SetStateToConflict(); UpdateServiceTypes(); VerifyOrExit(oldState == kRegistered); Get().InvokeConflictCallback(mServiceInstance.AsCString(), mServiceType.AsCString()); exit: return; } void Core::ServiceEntry::AnswerServiceNameQuestion(const AnswerInfo &aInfo) { RecordAndType records[] = { {mSrvRecord, ResourceRecord::kTypeSrv}, {mTxtRecord, ResourceRecord::kTypeTxt}, {mKeyRecord, ResourceRecord::kTypeKey}, }; VerifyOrExit(GetState() == kRegistered); if (aInfo.mIsProbe) { AnswerProbe(aInfo, records, GetArrayLength(records)); } else { AnswerNonProbe(aInfo, records, GetArrayLength(records)); } DetermineNextFireTime(); ScheduleTimer(); exit: return; } void Core::ServiceEntry::AnswerServiceTypeQuestion(const AnswerInfo &aInfo, const char *aSubLabel) { VerifyOrExit(GetState() == kRegistered); if (aSubLabel == nullptr) { mPtrRecord.ScheduleAnswer(aInfo); } else { SubType *subType = mSubTypes.FindMatching(aSubLabel); VerifyOrExit(subType != nullptr); subType->mPtrRecord.ScheduleAnswer(aInfo); } DetermineNextFireTime(); ScheduleTimer(); exit: return; } bool Core::ServiceEntry::ShouldSuppressKnownAnswer(uint32_t aTtl, const char *aSubLabel) const { // Check `aTtl` of a matching record in known-answer section of // a query with the corresponding PTR record's TTL and suppress // answer if it is at least at least half the correct value. bool shouldSuppress = false; uint32_t ttl; if (aSubLabel == nullptr) { ttl = mPtrRecord.GetTtl(); } else { const SubType *subType = mSubTypes.FindMatching(aSubLabel); VerifyOrExit(subType != nullptr); ttl = subType->mPtrRecord.GetTtl(); } shouldSuppress = (aTtl > ttl / 2); exit: return shouldSuppress; } void Core::ServiceEntry::HandleTimer(EntryContext &aContext) { Entry::HandleTimer(aContext); } void Core::ServiceEntry::ClearAppendState(void) { // Clear the append state for all `ServiceEntry` records, // along with all tracked name compression offsets. Entry::ClearAppendState(); mPtrRecord.MarkAsNotAppended(); mSrvRecord.MarkAsNotAppended(); mTxtRecord.MarkAsNotAppended(); mServiceNameOffset = kUnspecifiedOffset; mServiceTypeOffset = kUnspecifiedOffset; mSubServiceTypeOffset = kUnspecifiedOffset; mHostNameOffset = kUnspecifiedOffset; for (SubType &subType : mSubTypes) { subType.mPtrRecord.MarkAsNotAppended(); subType.mSubServiceNameOffset = kUnspecifiedOffset; } } void Core::ServiceEntry::PrepareProbe(TxMessage &aProbe) { bool prepareAgain = false; do { HostEntry *hostEntry = nullptr; aProbe.SaveCurrentState(); DiscoverOffsetsAndHost(hostEntry); AppendServiceNameTo(aProbe, kQuestionSection); AppendQuestionTo(aProbe); // Append records (if present) in authority section AppendSrvRecordTo(aProbe, kAuthoritySection); AppendTxtRecordTo(aProbe, kAuthoritySection); AppendKeyRecordTo(aProbe, kAuthoritySection); aProbe.CheckSizeLimitToPrepareAgain(prepareAgain); } while (prepareAgain); } void Core::ServiceEntry::StartAnnouncing(void) { for (SubType &subType : mSubTypes) { subType.mPtrRecord.StartAnnouncing(); } mPtrRecord.StartAnnouncing(); mSrvRecord.StartAnnouncing(); mTxtRecord.StartAnnouncing(); mKeyRecord.StartAnnouncing(); UpdateServiceTypes(); } void Core::ServiceEntry::PrepareResponse(EntryContext &aContext) { bool prepareAgain = false; TxMessage &response = aContext.mResponseMessage; do { response.SaveCurrentState(); PrepareResponseRecords(aContext); response.CheckSizeLimitToPrepareAgain(prepareAgain); } while (prepareAgain); UpdateRecordsState(response); } void Core::ServiceEntry::PrepareResponseRecords(EntryContext &aContext) { bool appendNsec = false; bool appendAdditionalRecordsForPtr = false; HostEntry *hostEntry = nullptr; TxMessage &response = aContext.mResponseMessage; DiscoverOffsetsAndHost(hostEntry); // We determine records to include in Additional Data section // per RFC 6763 section 12: // // - For PTR (base or sub-type), we include SRV, TXT, and host // addresses. // - For SRV, we include host addresses only (TXT record not // recommended). // // Records already appended in Answer section are excluded from // Additional Data. Host Entries are processed before Service // Entries which ensures address inclusion accuracy. // `MarkToAppendInAdditionalData()` marks a record for potential // Additional Data inclusion, but this is skipped if the record // is already appended in the Answer section. if (mPtrRecord.ShouldAppendTo(aContext)) { AppendPtrRecordTo(response, kAnswerSection); if (mPtrRecord.GetTtl() > 0) { appendAdditionalRecordsForPtr = true; } } for (SubType &subType : mSubTypes) { if (subType.mPtrRecord.ShouldAppendTo(aContext)) { AppendPtrRecordTo(response, kAnswerSection, &subType); if (subType.mPtrRecord.GetTtl() > 0) { appendAdditionalRecordsForPtr = true; } } } if (appendAdditionalRecordsForPtr) { mSrvRecord.MarkToAppendInAdditionalData(); mTxtRecord.MarkToAppendInAdditionalData(); if (hostEntry != nullptr) { hostEntry->mAddrRecord.MarkToAppendInAdditionalData(); } } if (mSrvRecord.ShouldAppendTo(aContext)) { AppendSrvRecordTo(response, kAnswerSection); appendNsec = true; if ((mSrvRecord.GetTtl() > 0) && (hostEntry != nullptr)) { hostEntry->mAddrRecord.MarkToAppendInAdditionalData(); } } if (mTxtRecord.ShouldAppendTo(aContext)) { AppendTxtRecordTo(response, kAnswerSection); appendNsec = true; } if (mKeyRecord.ShouldAppendTo(aContext)) { AppendKeyRecordTo(response, kAnswerSection); appendNsec = true; } // Append records in Additional Data section if (mSrvRecord.ShouldAppendInAdditionalDataSection()) { AppendSrvRecordTo(response, kAdditionalDataSection); } if (mTxtRecord.ShouldAppendInAdditionalDataSection()) { AppendTxtRecordTo(response, kAdditionalDataSection); } if ((hostEntry != nullptr) && (hostEntry->mAddrRecord.ShouldAppendInAdditionalDataSection())) { hostEntry->AppendAddressRecordsTo(response, kAdditionalDataSection); } if (appendNsec || ShouldAnswerNsec(aContext.GetNow())) { AppendNsecRecordTo(response, kAdditionalDataSection); } } void Core::ServiceEntry::UpdateRecordsState(const TxMessage &aResponse) { Entry::UpdateRecordsState(aResponse); mPtrRecord.UpdateStateAfterAnswer(aResponse); mSrvRecord.UpdateStateAfterAnswer(aResponse); mTxtRecord.UpdateStateAfterAnswer(aResponse); for (SubType &subType : mSubTypes) { subType.mPtrRecord.UpdateStateAfterAnswer(aResponse); } mSubTypes.RemoveAndFreeAllMatching(EmptyChecker()); if (IsEmpty()) { SetStateToRemoving(); } } void Core::ServiceEntry::DetermineNextFireTime(void) { VerifyOrExit(GetState() == kRegistered); Entry::DetermineNextFireTime(); mPtrRecord.UpdateFireTimeOn(*this); mSrvRecord.UpdateFireTimeOn(*this); mTxtRecord.UpdateFireTimeOn(*this); for (SubType &subType : mSubTypes) { subType.mPtrRecord.UpdateFireTimeOn(*this); } exit: return; } void Core::ServiceEntry::DetermineNextAggrTxTime(NextFireTime &aNextAggrTxTime) const { VerifyOrExit(GetState() == kRegistered); Entry::DetermineNextAggrTxTime(aNextAggrTxTime); mPtrRecord.DetermineNextAggrTxTime(aNextAggrTxTime); mSrvRecord.DetermineNextAggrTxTime(aNextAggrTxTime); mTxtRecord.DetermineNextAggrTxTime(aNextAggrTxTime); for (const SubType &subType : mSubTypes) { subType.mPtrRecord.DetermineNextAggrTxTime(aNextAggrTxTime); } exit: return; } void Core::ServiceEntry::DiscoverOffsetsAndHost(HostEntry *&aHostEntry) { // Discovers the `HostEntry` associated with this `ServiceEntry` // and name compression offsets from the previously appended // entries. aHostEntry = Get().mHostEntries.FindMatching(mHostName); if ((aHostEntry != nullptr) && (aHostEntry->GetState() != GetState())) { aHostEntry = nullptr; } if (aHostEntry != nullptr) { UpdateCompressOffset(mHostNameOffset, aHostEntry->mNameOffset); } for (ServiceEntry &other : Get().mServiceEntries) { // We only need to search up to `this` entry in the list, // since entries after `this` are not yet processed and not // yet appended in the response or the probe message. if (&other == this) { break; } if (other.GetState() != GetState()) { // Validate that both entries are in the same state, // ensuring their records are appended in the same // message, i.e., a probe or a response message. continue; } if (NameMatch(mHostName, other.mHostName)) { UpdateCompressOffset(mHostNameOffset, other.mHostNameOffset); } if (NameMatch(mServiceType, other.mServiceType)) { UpdateCompressOffset(mServiceTypeOffset, other.mServiceTypeOffset); if (GetState() == kProbing) { // No need to search for sub-type service offsets when // we are still probing. continue; } UpdateCompressOffset(mSubServiceTypeOffset, other.mSubServiceTypeOffset); for (SubType &subType : mSubTypes) { const SubType *otherSubType = other.mSubTypes.FindMatching(subType.mLabel.AsCString()); if (otherSubType != nullptr) { UpdateCompressOffset(subType.mSubServiceNameOffset, otherSubType->mSubServiceNameOffset); } } } } } void Core::ServiceEntry::UpdateServiceTypes(void) { // This method updates the `mServiceTypes` list adding or // removing this `ServiceEntry` info. // // It is called whenever the `ServiceEntry` state gets changed // or a PTR record is added or removed. The service is valid // when entry is registered and we have a PTR with non-zero // TTL. bool shouldAdd = (GetState() == kRegistered) && mPtrRecord.CanAnswer(); ServiceType *serviceType; VerifyOrExit(shouldAdd != mIsAddedInServiceTypes); mIsAddedInServiceTypes = shouldAdd; serviceType = Get().mServiceTypes.FindMatching(mServiceType); if (shouldAdd && (serviceType == nullptr)) { serviceType = ServiceType::AllocateAndInit(GetInstance(), mServiceType.AsCString()); OT_ASSERT(serviceType != nullptr); Get().mServiceTypes.Push(*serviceType); } VerifyOrExit(serviceType != nullptr); if (shouldAdd) { serviceType->IncrementNumEntries(); } else { serviceType->DecrementNumEntries(); if (serviceType->GetNumEntries() == 0) { // If there are no more `ServiceEntry` with // this service type, we remove the it from // the `mServiceTypes` list. It is safe to // remove here as this method will never be // called while we are iterating over the // `mServiceTypes` list. Get().mServiceTypes.RemoveMatching(*serviceType); } } exit: return; } void Core::ServiceEntry::AppendSrvRecordTo(TxMessage &aTxMessage, Section aSection) { Message *message; SrvRecord srv; uint16_t offset; bool isLegacyUnicast = (aTxMessage.GetType() == TxMessage::kLegacyUnicastResponse); VerifyOrExit(mSrvRecord.CanAppend()); mSrvRecord.MarkAsAppended(aTxMessage, aSection); message = &aTxMessage.SelectMessageFor(aSection); srv.Init(); srv.SetPriority(mPriority); srv.SetWeight(mWeight); srv.SetPort(mPort); srv.SetTtl(mSrvRecord.GetTtl(isLegacyUnicast)); UpdateCacheFlushFlagIn(srv, aSection, isLegacyUnicast); // RFC6762, Section 18.14 Name Compression: // In legacy unicast responses generated to answer legacy queries, name // compression MUST NOT be performed on SRV records. AppendServiceNameTo(aTxMessage, aSection, /* aPerformNameCompression */ !isLegacyUnicast); offset = message->GetLength(); SuccessOrAssert(message->Append(srv)); AppendHostNameTo(aTxMessage, aSection); UpdateRecordLengthInMessage(srv, *message, offset); aTxMessage.IncrementRecordCount(aSection); exit: return; } void Core::ServiceEntry::AppendTxtRecordTo(TxMessage &aTxMessage, Section aSection) { Message *message; TxtRecord txt; bool isLegacyUnicast = (aTxMessage.GetType() == TxMessage::kLegacyUnicastResponse); VerifyOrExit(mTxtRecord.CanAppend()); mTxtRecord.MarkAsAppended(aTxMessage, aSection); message = &aTxMessage.SelectMessageFor(aSection); txt.Init(); txt.SetLength(mTxtData.GetLength()); txt.SetTtl(mTxtRecord.GetTtl(isLegacyUnicast)); UpdateCacheFlushFlagIn(txt, aSection, isLegacyUnicast); AppendServiceNameTo(aTxMessage, aSection); SuccessOrAssert(message->Append(txt)); SuccessOrAssert(message->AppendBytes(mTxtData.GetBytes(), mTxtData.GetLength())); aTxMessage.IncrementRecordCount(aSection); exit: return; } void Core::ServiceEntry::AppendPtrRecordTo(TxMessage &aTxMessage, Section aSection, SubType *aSubType) { // Appends PTR record for base service (when `aSubType == nullptr`) or // for the given `aSubType`. Message *message; RecordInfo &ptrRecord = (aSubType == nullptr) ? mPtrRecord : aSubType->mPtrRecord; PtrRecord ptr; uint16_t offset; bool isLegacyUnicast = (aTxMessage.GetType() == TxMessage::kLegacyUnicastResponse); VerifyOrExit(ptrRecord.CanAppend()); ptrRecord.MarkAsAppended(aTxMessage, aSection); message = &aTxMessage.SelectMessageFor(aSection); ptr.Init(); ptr.SetTtl(ptrRecord.GetTtl(isLegacyUnicast)); if (aSubType == nullptr) { AppendServiceTypeTo(aTxMessage, aSection); } else { AppendSubServiceNameTo(aTxMessage, aSection, *aSubType); } offset = message->GetLength(); SuccessOrAssert(message->Append(ptr)); AppendServiceNameTo(aTxMessage, aSection); UpdateRecordLengthInMessage(ptr, *message, offset); aTxMessage.IncrementRecordCount(aSection); exit: return; } void Core::ServiceEntry::AppendKeyRecordTo(TxMessage &aTxMessage, Section aSection) { Entry::AppendKeyRecordTo(aTxMessage, aSection, &AppendEntryName); } void Core::ServiceEntry::AppendNsecRecordTo(TxMessage &aTxMessage, Section aSection) { TypeArray types; if (mSrvRecord.IsPresent() && (mSrvRecord.GetTtl() > 0)) { types.Add(ResourceRecord::kTypeSrv); } if (mTxtRecord.IsPresent() && (mTxtRecord.GetTtl() > 0)) { types.Add(ResourceRecord::kTypeTxt); } if (mKeyRecord.IsPresent() && (mKeyRecord.GetTtl() > 0)) { types.Add(ResourceRecord::kTypeKey); } if (!types.IsEmpty()) { Entry::AppendNsecRecordTo(aTxMessage, aSection, types, &AppendEntryName); } } void Core::ServiceEntry::AppendEntryName(Entry &aEntry, TxMessage &aTxMessage, Section aSection) { static_cast(aEntry).AppendServiceNameTo(aTxMessage, aSection); } void Core::ServiceEntry::AppendServiceNameTo(TxMessage &aTxMessage, Section aSection, bool aPerformNameCompression) { AppendOutcome outcome; if (!aPerformNameCompression) { uint16_t compressOffset = kUnspecifiedOffset; outcome = aTxMessage.AppendLabel(aSection, mServiceInstance.AsCString(), compressOffset); VerifyOrExit(outcome == kAppendedLabels); } else { outcome = aTxMessage.AppendLabel(aSection, mServiceInstance.AsCString(), mServiceNameOffset); VerifyOrExit(outcome != kAppendedFullNameAsCompressed); } AppendServiceTypeTo(aTxMessage, aSection); exit: return; } void Core::ServiceEntry::AppendServiceTypeTo(TxMessage &aTxMessage, Section aSection) { aTxMessage.AppendServiceType(aSection, mServiceType.AsCString(), mServiceTypeOffset); } void Core::ServiceEntry::AppendSubServiceTypeTo(TxMessage &aTxMessage, Section aSection) { AppendOutcome outcome; outcome = aTxMessage.AppendLabel(aSection, kSubServiceLabel, mSubServiceTypeOffset); VerifyOrExit(outcome != kAppendedFullNameAsCompressed); AppendServiceTypeTo(aTxMessage, aSection); exit: return; } void Core::ServiceEntry::AppendSubServiceNameTo(TxMessage &aTxMessage, Section aSection, SubType &aSubType) { AppendOutcome outcome; outcome = aTxMessage.AppendLabel(aSection, aSubType.mLabel.AsCString(), aSubType.mSubServiceNameOffset); VerifyOrExit(outcome != kAppendedFullNameAsCompressed); AppendSubServiceTypeTo(aTxMessage, aSection); exit: return; } void Core::ServiceEntry::AppendHostNameTo(TxMessage &aTxMessage, Section aSection) { AppendOutcome outcome; outcome = aTxMessage.AppendMultipleLabels(aSection, mHostName.AsCString(), mHostNameOffset); VerifyOrExit(outcome != kAppendedFullNameAsCompressed); aTxMessage.AppendDomainName(aSection); exit: return; } #if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE Error Core::ServiceEntry::CopyInfoTo(Service &aService, EntryState &aState, EntryIterator &aIterator) const { Error error = kErrorNone; VerifyOrExit(mPtrRecord.IsPresent(), error = kErrorNotFound); aIterator.mSubTypeArray.Free(); for (const SubType &subType : mSubTypes) { SuccessOrAssert(aIterator.mSubTypeArray.PushBack(subType.mLabel.AsCString())); } aService.mHostName = mHostName.AsCString(); aService.mServiceInstance = mServiceInstance.AsCString(); aService.mServiceType = mServiceType.AsCString(); aService.mSubTypeLabels = aIterator.mSubTypeArray.AsCArray(); aService.mSubTypeLabelsLength = aIterator.mSubTypeArray.GetLength(); aService.mTxtData = mTxtData.GetBytes(); aService.mTxtDataLength = mTxtData.GetLength(); aService.mPort = mPort; aService.mPriority = mPriority; aService.mWeight = mWeight; aService.mTtl = mPtrRecord.GetTtl(); aService.mInfraIfIndex = Get().mInfraIfIndex; aState = static_cast(GetState()); exit: return error; } Error Core::ServiceEntry::CopyInfoTo(Key &aKey, EntryState &aState) const { Error error; SuccessOrExit(error = CopyKeyInfoTo(aKey, aState)); aKey.mName = mServiceInstance.AsCString(); aKey.mServiceType = mServiceType.AsCString(); exit: return error; } #endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE //---------------------------------------------------------------------------------------------------------------------- // Core::ServiceEntry::SubType Error Core::ServiceEntry::SubType::Init(const char *aLabel) { mSubServiceNameOffset = kUnspecifiedOffset; return mLabel.Set(aLabel); } bool Core::ServiceEntry::SubType::Matches(const EmptyChecker &aChecker) const { OT_UNUSED_VARIABLE(aChecker); return !mPtrRecord.IsPresent(); } bool Core::ServiceEntry::SubType::IsContainedIn(const Service &aService) const { bool contains = false; for (uint16_t i = 0; i < aService.mSubTypeLabelsLength; i++) { if (NameMatch(mLabel, aService.mSubTypeLabels[i])) { contains = true; break; } } return contains; } //---------------------------------------------------------------------------------------------------------------------- // Core::ServiceType Error Core::ServiceType::Init(Instance &aInstance, const char *aServiceType) { Error error; InstanceLocatorInit::Init(aInstance); mNext = nullptr; mNumEntries = 0; SuccessOrExit(error = mServiceType.Set(aServiceType)); mServicesPtr.UpdateTtl(kServicesPtrTtl); mServicesPtr.StartAnnouncing(); mServicesPtr.UpdateFireTimeOn(*this); ScheduleFireTimeOn(Get().mEntryTimer); exit: return error; } bool Core::ServiceType::Matches(const Name &aServiceTypeName) const { return aServiceTypeName.Matches(/* aFirstLabel */ nullptr, mServiceType.AsCString(), kLocalDomain); } bool Core::ServiceType::Matches(const Heap::String &aServiceType) const { return NameMatch(aServiceType, mServiceType); } void Core::ServiceType::ClearAppendState(void) { mServicesPtr.MarkAsNotAppended(); } void Core::ServiceType::AnswerQuestion(const AnswerInfo &aInfo) { VerifyOrExit(mServicesPtr.CanAnswer()); mServicesPtr.ScheduleAnswer(aInfo); mServicesPtr.UpdateFireTimeOn(*this); ScheduleFireTimeOn(Get().mEntryTimer); exit: return; } bool Core::ServiceType::ShouldSuppressKnownAnswer(uint32_t aTtl) const { // Check `aTtl` of a matching record in known-answer section of // a query with the corresponding PTR record's TTL and suppress // answer if it is at least at least half the correct value. return (aTtl > mServicesPtr.GetTtl() / 2); } void Core::ServiceType::HandleTimer(EntryContext &aContext) { ClearAppendState(); VerifyOrExit(HasFireTime()); VerifyOrExit(GetFireTime() <= aContext.GetNow()); ClearFireTime(); PrepareResponse(aContext); mServicesPtr.UpdateFireTimeOn(*this); exit: UpdateNextFireTimeOn(aContext.mNextFireTime); } void Core::ServiceType::PrepareResponse(EntryContext &aContext) { bool prepareAgain = false; TxMessage &response = aContext.mResponseMessage; do { response.SaveCurrentState(); PrepareResponseRecords(aContext); response.CheckSizeLimitToPrepareAgain(prepareAgain); } while (prepareAgain); mServicesPtr.UpdateStateAfterAnswer(response); } void Core::ServiceType::PrepareResponseRecords(EntryContext &aContext) { uint16_t serviceTypeOffset = kUnspecifiedOffset; VerifyOrExit(mServicesPtr.ShouldAppendTo(aContext)); // Discover compress offset for `mServiceType` if previously // appended from any `ServiceEntry`. for (const ServiceEntry &serviceEntry : Get().mServiceEntries) { if (serviceEntry.GetState() != Entry::kRegistered) { continue; } if (NameMatch(mServiceType, serviceEntry.mServiceType)) { UpdateCompressOffset(serviceTypeOffset, serviceEntry.mServiceTypeOffset); if (serviceTypeOffset != kUnspecifiedOffset) { break; } } } AppendPtrRecordTo(aContext.mResponseMessage, serviceTypeOffset); exit: return; } void Core::ServiceType::AppendPtrRecordTo(TxMessage &aResponse, uint16_t aServiceTypeOffset) { Message *message; PtrRecord ptr; uint16_t offset; VerifyOrExit(mServicesPtr.CanAppend()); mServicesPtr.MarkAsAppended(aResponse, kAnswerSection); message = &aResponse.SelectMessageFor(kAnswerSection); ptr.Init(); if (aResponse.GetType() == TxMessage::kLegacyUnicastResponse) { ptr.SetTtl(Min(Core::RecordInfo::kMaxLegacyUnicastTtl, mServicesPtr.GetTtl())); } else { ptr.SetTtl(mServicesPtr.GetTtl()); } aResponse.AppendServicesDnssdName(kAnswerSection); offset = message->GetLength(); SuccessOrAssert(message->Append(ptr)); aResponse.AppendServiceType(kAnswerSection, mServiceType.AsCString(), aServiceTypeOffset); UpdateRecordLengthInMessage(ptr, *message, offset); aResponse.IncrementRecordCount(kAnswerSection); exit: return; } void Core::ServiceType::DetermineNextAggrTxTime(NextFireTime &aNextAggrTxTime) const { mServicesPtr.DetermineNextAggrTxTime(aNextAggrTxTime); } //---------------------------------------------------------------------------------------------------------------------- // Core::TxMessage Core::TxMessage::TxMessage(Instance &aInstance, Type aType, uint16_t aQueryId) : InstanceLocator(aInstance) { Init(aType, aQueryId); } Core::TxMessage::TxMessage(Instance &aInstance, Type aType, const AddressInfo &aUnicastDest, uint16_t aQueryId) : TxMessage(aInstance, aType, aQueryId) { mUnicastDest = aUnicastDest; } void Core::TxMessage::Init(Type aType, uint16_t aMessageId) { Header header; mRecordCounts.Clear(); mSavedRecordCounts.Clear(); mSavedMsgLength = 0; mSavedExtraMsgLength = 0; mDomainOffset = kUnspecifiedOffset; mUdpOffset = kUnspecifiedOffset; mTcpOffset = kUnspecifiedOffset; mServicesDnssdOffset = kUnspecifiedOffset; mType = aType; // Allocate messages. The main `mMsgPtr` is always allocated. // The Authority and Addition section messages are allocated // the first time they are used. mMsgPtr.Reset(Get().Allocate(Message::kTypeOther)); OT_ASSERT(!mMsgPtr.IsNull()); mExtraMsgPtr.Reset(); header.Clear(); switch (aType) { case kMulticastProbe: case kMulticastQuery: header.SetType(Header::kTypeQuery); break; case kMulticastResponse: case kUnicastResponse: case kLegacyUnicastResponse: header.SetType(Header::kTypeResponse); header.SetMessageId(aMessageId); break; } SuccessOrAssert(mMsgPtr->Append(header)); } Message &Core::TxMessage::SelectMessageFor(Section aSection) { // Selects the `Message` to use for a given `aSection` based // the message type. Message *message = nullptr; Section mainSection = kAnswerSection; Section extraSection = kAdditionalDataSection; switch (mType) { case kMulticastProbe: mainSection = kQuestionSection; extraSection = kAuthoritySection; break; case kMulticastQuery: mainSection = kQuestionSection; extraSection = kAnswerSection; break; case kLegacyUnicastResponse: case kUnicastResponse: case kMulticastResponse: break; } if (aSection == mainSection) { message = mMsgPtr.Get(); } else if (aSection == extraSection) { if (mExtraMsgPtr.IsNull()) { mExtraMsgPtr.Reset(Get().Allocate(Message::kTypeOther)); OT_ASSERT(!mExtraMsgPtr.IsNull()); } message = mExtraMsgPtr.Get(); } OT_ASSERT(message != nullptr); return *message; } Core::AppendOutcome Core::TxMessage::AppendLabel(Section aSection, const char *aLabel, uint16_t &aCompressOffset) { return AppendLabels(aSection, aLabel, kIsSingleLabel, aCompressOffset); } Core::AppendOutcome Core::TxMessage::AppendMultipleLabels(Section aSection, const char *aLabels, uint16_t &aCompressOffset) { return AppendLabels(aSection, aLabels, !kIsSingleLabel, aCompressOffset); } Core::AppendOutcome Core::TxMessage::AppendLabels(Section aSection, const char *aLabels, bool aIsSingleLabel, uint16_t &aCompressOffset) { // Appends DNS name label(s) to the message in the specified section, // using compression if possible. // // - If a valid `aCompressOffset` is given (indicating name was appended before) // a compressed pointer label is used, and `kAppendedFullNameAsCompressed` // is returned. // - Otherwise, `aLabels` is appended, `aCompressOffset` is also updated for // future compression, and `kAppendedLabels` is returned. // // `aIsSingleLabel` indicates that `aLabels` string should be appended // as a single label. This is useful for service instance label which // can itself contain the dot `.` character. AppendOutcome outcome = kAppendedLabels; Message &message = SelectMessageFor(aSection); if (aCompressOffset != kUnspecifiedOffset) { SuccessOrAssert(Name::AppendPointerLabel(aCompressOffset, message)); outcome = kAppendedFullNameAsCompressed; ExitNow(); } SaveOffset(aCompressOffset, message, aSection); if (aIsSingleLabel) { SuccessOrAssert(Name::AppendLabel(aLabels, message)); } else { SuccessOrAssert(Name::AppendMultipleLabels(aLabels, message)); } exit: return outcome; } void Core::TxMessage::AppendServiceType(Section aSection, const char *aServiceType, uint16_t &aCompressOffset) { // Appends DNS service type name to the message in the specified // section, using compression if possible. const char *serviceLabels = aServiceType; bool isUdp = false; bool isTcp = false; Name::Buffer labelsBuffer; AppendOutcome outcome; if (Name::ExtractLabels(serviceLabels, kUdpServiceLabel, labelsBuffer) == kErrorNone) { isUdp = true; serviceLabels = labelsBuffer; } else if (Name::ExtractLabels(serviceLabels, kTcpServiceLabel, labelsBuffer) == kErrorNone) { isTcp = true; serviceLabels = labelsBuffer; } outcome = AppendMultipleLabels(aSection, serviceLabels, aCompressOffset); VerifyOrExit(outcome != kAppendedFullNameAsCompressed); if (isUdp) { outcome = AppendLabel(aSection, kUdpServiceLabel, mUdpOffset); } else if (isTcp) { outcome = AppendLabel(aSection, kTcpServiceLabel, mTcpOffset); } VerifyOrExit(outcome != kAppendedFullNameAsCompressed); AppendDomainName(aSection); exit: return; } void Core::TxMessage::AppendDomainName(Section aSection) { Message &message = SelectMessageFor(aSection); if (mDomainOffset != kUnspecifiedOffset) { SuccessOrAssert(Name::AppendPointerLabel(mDomainOffset, message)); ExitNow(); } SaveOffset(mDomainOffset, message, aSection); SuccessOrAssert(Name::AppendName(kLocalDomain, message)); exit: return; } void Core::TxMessage::AppendServicesDnssdName(Section aSection) { Message &message = SelectMessageFor(aSection); if (mServicesDnssdOffset != kUnspecifiedOffset) { SuccessOrAssert(Name::AppendPointerLabel(mServicesDnssdOffset, message)); ExitNow(); } SaveOffset(mServicesDnssdOffset, message, aSection); SuccessOrAssert(Name::AppendMultipleLabels(kServicesDnssdLabels, message)); AppendDomainName(aSection); exit: return; } void Core::TxMessage::AddQuestionFrom(const Message &aMessage) { uint16_t offset = sizeof(Header); IgnoreError(Name::ParseName(aMessage, offset)); offset += sizeof(ot::Dns::Question); SuccessOrAssert(mMsgPtr->AppendBytesFromMessage(aMessage, sizeof(Header), offset - sizeof(Header))); IncrementRecordCount(kQuestionSection); } void Core::TxMessage::SaveOffset(uint16_t &aCompressOffset, const Message &aMessage, Section aSection) { // Saves the current message offset in `aCompressOffset` for name // compression, but only when appending to the question or answer // sections. // // This is necessary because other sections use separate message, // and their offsets can shift when records are added to the main // message. // // While current record types guarantee name inclusion in // question/answer sections before their use in other sections, // this check allows future extensions. switch (aSection) { case kQuestionSection: case kAnswerSection: aCompressOffset = aMessage.GetLength(); break; case kAuthoritySection: case kAdditionalDataSection: break; } } bool Core::TxMessage::IsOverSizeLimit(void) const { uint32_t size = mMsgPtr->GetLength(); if (!mExtraMsgPtr.IsNull()) { size += mExtraMsgPtr->GetLength(); } return (size > Get().mMaxMessageSize); } void Core::TxMessage::SaveCurrentState(void) { mSavedRecordCounts = mRecordCounts; mSavedMsgLength = mMsgPtr->GetLength(); mSavedExtraMsgLength = mExtraMsgPtr.IsNull() ? 0 : mExtraMsgPtr->GetLength(); } void Core::TxMessage::RestoreToSavedState(void) { mRecordCounts = mSavedRecordCounts; IgnoreError(mMsgPtr->SetLength(mSavedMsgLength)); if (!mExtraMsgPtr.IsNull()) { IgnoreError(mExtraMsgPtr->SetLength(mSavedExtraMsgLength)); } } void Core::TxMessage::CheckSizeLimitToPrepareAgain(bool &aPrepareAgain) { // Manages message size limits by re-preparing messages when // necessary: // - Checks if `TxMessage` exceeds the size limit. // - If so, restores the `TxMessage` to its previously saved // state, sends it, and re-initializes it which will also // clear the "AppendState" of the related host and service // entries to ensure correct re-processing. // - Sets `aPrepareAgain` to `true` to signal that records should // be prepared and added to the new message. // // We allow the `aPrepareAgain` to happen once. The very unlikely // case where the `Entry` itself has so many records that its // contents exceed the message size limit, is not handled, i.e. // we always include all records of a single `Entry` within the same // message. In future, the code can be updated to allow truncated // messages. if (aPrepareAgain) { aPrepareAgain = false; ExitNow(); } VerifyOrExit(IsOverSizeLimit()); aPrepareAgain = true; RestoreToSavedState(); Send(); Reinit(); exit: return; } void Core::TxMessage::Send(void) { static constexpr uint16_t kHeaderOffset = 0; Header header; VerifyOrExit(!mRecordCounts.IsEmpty()); SuccessOrAssert(mMsgPtr->Read(kHeaderOffset, header)); mRecordCounts.WriteTo(header); mMsgPtr->Write(kHeaderOffset, header); if (!mExtraMsgPtr.IsNull()) { SuccessOrAssert(mMsgPtr->AppendBytesFromMessage(*mExtraMsgPtr, 0, mExtraMsgPtr->GetLength())); } Get().mTxMessageHistory.Add(*mMsgPtr); // We pass ownership of message to the platform layer. switch (mType) { case kMulticastProbe: case kMulticastQuery: case kMulticastResponse: otPlatMdnsSendMulticast(&GetInstance(), mMsgPtr.Release(), Get().mInfraIfIndex); break; case kUnicastResponse: case kLegacyUnicastResponse: otPlatMdnsSendUnicast(&GetInstance(), mMsgPtr.Release(), &mUnicastDest); break; } exit: return; } void Core::TxMessage::Reinit(void) { Init(GetType()); // After re-initializing `TxMessage`, we clear the "AppendState" // on all related host and service entries, and service types // or all cache entries (depending on the `GetType()`). switch (GetType()) { case kMulticastProbe: case kMulticastResponse: case kUnicastResponse: for (HostEntry &entry : Get().mHostEntries) { if (ShouldClearAppendStateOnReinit(entry)) { entry.ClearAppendState(); } } for (ServiceEntry &entry : Get().mServiceEntries) { if (ShouldClearAppendStateOnReinit(entry)) { entry.ClearAppendState(); } } for (ServiceType &serviceType : Get().mServiceTypes) { if ((GetType() == kMulticastResponse) || (GetType() == kUnicastResponse)) { serviceType.ClearAppendState(); } } break; case kMulticastQuery: for (BrowseCache &browseCache : Get().mBrowseCacheList) { browseCache.ClearCompressOffsets(); } for (SrvCache &srvCache : Get().mSrvCacheList) { srvCache.ClearCompressOffsets(); } for (TxtCache &txtCache : Get().mTxtCacheList) { txtCache.ClearCompressOffsets(); } // `Ip6AddrCache` entries do not track any append state or // compress offset since the host name should not be used // in any other query question. break; case kLegacyUnicastResponse: break; } } bool Core::TxMessage::ShouldClearAppendStateOnReinit(const Entry &aEntry) const { // Determines whether we should clear "append state" on `aEntry` // when re-initializing the `TxMessage`. If message is a probe, we // check that entry is in `kProbing` state, if message is a // unicast/multicast response, we check for `kRegistered` state. bool shouldClear = false; switch (aEntry.GetState()) { case Entry::kProbing: shouldClear = (GetType() == kMulticastProbe); break; case Entry::kRegistered: shouldClear = (GetType() == kMulticastResponse) || (GetType() == kUnicastResponse); break; case Entry::kConflict: case Entry::kRemoving: shouldClear = true; break; } return shouldClear; } //---------------------------------------------------------------------------------------------------------------------- // Core::EntryContext Core::EntryContext::EntryContext(Instance &aInstance, TxMessage::Type aResponseType) : mProbeMessage(aInstance, TxMessage::kMulticastProbe) , mResponseMessage(aInstance, aResponseType) { mNextAggrTxTime = mNextFireTime.GetNow().GetDistantFuture(); } Core::EntryContext::EntryContext(Instance &aInstance, TxMessage::Type aResponseType, const AddressInfo &aDest, uint16_t aQueryId) : mProbeMessage(aInstance, TxMessage::kMulticastProbe) , mResponseMessage(aInstance, aResponseType, aDest, aQueryId) { mNextAggrTxTime = mNextFireTime.GetNow().GetDistantFuture(); } //---------------------------------------------------------------------------------------------------------------------- // Core::RxMessage Error Core::RxMessage::Init(Instance &aInstance, OwnedPtr &aMessagePtr, bool aIsUnicast, const AddressInfo &aSenderAddress) { static const Section kSections[] = {kAnswerSection, kAuthoritySection, kAdditionalDataSection}; Error error = kErrorNone; Header header; uint16_t offset; uint16_t numRecords; InstanceLocatorInit::Init(aInstance); mNext = nullptr; mRxTime = TimerMilli::GetNow(); VerifyOrExit(!aMessagePtr.IsNull(), error = kErrorInvalidArgs); offset = aMessagePtr->GetOffset(); SuccessOrExit(error = aMessagePtr->Read(offset, header)); offset += sizeof(Header); // RFC 6762 Section 18: Query type (OPCODE) must be zero // (standard query). All other flags must be ignored. Messages // with non-zero RCODE MUST be silently ignored. VerifyOrExit(header.GetQueryType() == Header::kQueryTypeStandard, error = kErrorParse); VerifyOrExit(header.GetResponseCode() == Header::kResponseSuccess, error = kErrorParse); mIsQuery = (header.GetType() == Header::kTypeQuery); mIsUnicast = aIsUnicast; mTruncated = header.IsTruncationFlagSet(); mSenderAddress = aSenderAddress; if (aSenderAddress.mPort != kUdpPort) { // Simple DNS resolver does not allow more than one question in a query message if (mIsQuery && header.GetQuestionCount() == 1) { // Section 6.7 Legacy Unicast mIsLegacyUnicast = true; mQueryId = header.GetMessageId(); } else { // The source port in a response MUST be mDNS port. // Otherwise response message MUST be silently ignored. ExitNow(error = kErrorParse); } } if (mIsUnicast && mIsQuery) { // Direct Unicast Queries to Port 5353 (RFC 6762 - section 5.5). // Responders SHOULD check that the source address in the query // packet matches the local subnet for that link and silently ignore // the packet if not. LogInfo("We do not yet support unicast query to mDNS port"); ExitNow(error = kErrorNotCapable); } mRecordCounts.ReadFrom(header); // Parse questions mStartOffset[kQuestionSection] = offset; SuccessOrAssert(mQuestions.ReserveCapacity(mRecordCounts.GetFor(kQuestionSection))); for (numRecords = mRecordCounts.GetFor(kQuestionSection); numRecords > 0; numRecords--) { Question *question = mQuestions.PushBack(); ot::Dns::Question record; uint16_t rrClass; OT_ASSERT(question != nullptr); question->mNameOffset = offset; SuccessOrExit(error = Name::ParseName(*aMessagePtr, offset)); SuccessOrExit(error = aMessagePtr->Read(offset, record)); offset += sizeof(record); question->mRrType = record.GetType(); rrClass = record.GetClass(); question->mUnicastResponse = rrClass & kClassQuestionUnicastFlag; question->mIsRrClassInternet = RrClassIsInternetOrAny(rrClass); } // Parse and validate records in Answer, Authority and Additional // Data sections. for (Section section : kSections) { mStartOffset[section] = offset; SuccessOrExit(error = ResourceRecord::ParseRecords(*aMessagePtr, offset, mRecordCounts.GetFor(section))); } // Determine which questions are probes by searching in the // Authority section for records matching the question name. for (Question &question : mQuestions) { Name name(*aMessagePtr, question.mNameOffset); offset = mStartOffset[kAuthoritySection]; numRecords = mRecordCounts.GetFor(kAuthoritySection); if (ResourceRecord::FindRecord(*aMessagePtr, offset, numRecords, name) == kErrorNone) { question.mIsProbe = true; } } mIsSelfOriginating = Get().mTxMessageHistory.Contains(*aMessagePtr); mMessagePtr = aMessagePtr.PassOwnership(); exit: if (error != kErrorNone) { LogInfo("Failed to parse message from %s, error:%s", aSenderAddress.GetAddress().ToString().AsCString(), ErrorToString(error)); } return error; } void Core::RxMessage::ClearProcessState(void) { for (Question &question : mQuestions) { question.ClearProcessState(); } } Core::RxMessage::ProcessOutcome Core::RxMessage::ProcessQuery(bool aShouldProcessTruncated) { ProcessOutcome outcome = kProcessed; bool shouldDelay = false; bool canAnswer = false; bool needUnicastResponse = false; uint16_t delay = 0; for (Question &question : mQuestions) { question.ClearProcessState(); ProcessQuestion(question); // Check if we can answer every question in the query and all // answers are for unique records (where we own the name). This // determines whether we need to add any random delay before // responding. if (!question.mCanAnswer || !question.mIsUnique) { shouldDelay = true; } if (question.mCanAnswer) { canAnswer = true; if (question.mUnicastResponse || mIsLegacyUnicast) { needUnicastResponse = true; } } } if (mIsLegacyUnicast) { shouldDelay = false; } VerifyOrExit(canAnswer); if (mTruncated && !aShouldProcessTruncated) { outcome = kSaveAsMultiPacket; ExitNow(); } if (shouldDelay) { delay = Random::NonCrypto::GetUint32InRange(kMinResponseDelay, kMaxResponseDelay); } for (const Question &question : mQuestions) { AnswerQuestion(question, delay); } if (needUnicastResponse) { SendUnicastResponse(); } exit: return outcome; } void Core::RxMessage::ProcessQuestion(Question &aQuestion) { Name name(*mMessagePtr, aQuestion.mNameOffset); VerifyOrExit(aQuestion.mIsRrClassInternet); // Check if question name matches "_services._dns-sd._udp" (all services) if (name.Matches(/* aFirstLabel */ nullptr, kServicesDnssdLabels, kLocalDomain)) { VerifyOrExit(QuestionMatches(aQuestion.mRrType, ResourceRecord::kTypePtr)); VerifyOrExit(!Get().mServiceTypes.IsEmpty()); aQuestion.mCanAnswer = true; aQuestion.mIsForAllServicesDnssd = true; ExitNow(); } // Check if question name matches a `HostEntry` or a `ServiceEntry` aQuestion.mEntry = Get().mHostEntries.FindMatching(name); if (aQuestion.mEntry == nullptr) { aQuestion.mEntry = Get().mServiceEntries.FindMatching(name); aQuestion.mIsForService = (aQuestion.mEntry != nullptr); } if (aQuestion.mEntry != nullptr) { switch (aQuestion.mEntry->GetState()) { case Entry::kProbing: if (aQuestion.mIsProbe) { // Handling probe conflicts deviates from RFC 6762. // We allow the conflict to happen and report it // let the caller handle it. In future, TSR can // help select the winner. } break; case Entry::kRegistered: aQuestion.mCanAnswer = true; aQuestion.mIsUnique = true; break; case Entry::kConflict: case Entry::kRemoving: break; } } else { // Check if question matches a service type or sub-type. We // can answer PTR or ANY questions. There may be multiple // service entries matching the question. We find and save // the first match. `AnswerServiceTypeQuestion()` will start // from the saved entry and finds all the other matches. bool isSubType; Name::LabelBuffer subLabel; Name baseType; VerifyOrExit(QuestionMatches(aQuestion.mRrType, ResourceRecord::kTypePtr)); isSubType = ParseQuestionNameAsSubType(aQuestion, subLabel, baseType); if (!isSubType) { baseType = name; } for (ServiceEntry &serviceEntry : Get().mServiceEntries) { if ((serviceEntry.GetState() != Entry::kRegistered) || !serviceEntry.MatchesServiceType(baseType)) { continue; } if (isSubType && !serviceEntry.CanAnswerSubType(subLabel)) { continue; } aQuestion.mCanAnswer = true; aQuestion.mEntry = &serviceEntry; aQuestion.mIsForService = true; aQuestion.mIsServiceType = true; ExitNow(); } } exit: return; } void Core::RxMessage::AnswerQuestion(const Question &aQuestion, uint16_t aDelay) { HostEntry *hostEntry; ServiceEntry *serviceEntry; AnswerInfo answerInfo; VerifyOrExit(aQuestion.mCanAnswer); answerInfo.mQuestionRrType = aQuestion.mRrType; answerInfo.mAnswerDelay = aDelay; answerInfo.mQueryRxTime = mRxTime; answerInfo.mIsProbe = aQuestion.mIsProbe; answerInfo.mUnicastResponse = aQuestion.mUnicastResponse; answerInfo.mLegacyUnicastResponse = mIsLegacyUnicast; if (aQuestion.mIsForAllServicesDnssd) { AnswerAllServicesQuestion(aQuestion, answerInfo); ExitNow(); } hostEntry = aQuestion.mIsForService ? nullptr : static_cast(aQuestion.mEntry); serviceEntry = aQuestion.mIsForService ? static_cast(aQuestion.mEntry) : nullptr; if (hostEntry != nullptr) { hostEntry->AnswerQuestion(answerInfo); ExitNow(); } // Question is for `ServiceEntry` VerifyOrExit(serviceEntry != nullptr); if (!aQuestion.mIsServiceType) { serviceEntry->AnswerServiceNameQuestion(answerInfo); } else { AnswerServiceTypeQuestion(aQuestion, answerInfo, *serviceEntry); } exit: return; } void Core::RxMessage::AnswerServiceTypeQuestion(const Question &aQuestion, const AnswerInfo &aInfo, ServiceEntry &aFirstEntry) { Name serviceType(*mMessagePtr, aQuestion.mNameOffset); Name baseType; Name::LabelBuffer labelBuffer; const char *subLabel; if (ParseQuestionNameAsSubType(aQuestion, labelBuffer, baseType)) { subLabel = labelBuffer; } else { baseType = serviceType; subLabel = nullptr; } for (ServiceEntry *serviceEntry = &aFirstEntry; serviceEntry != nullptr; serviceEntry = serviceEntry->GetNext()) { bool shouldSuppress = false; if ((serviceEntry->GetState() != Entry::kRegistered) || !serviceEntry->MatchesServiceType(baseType)) { continue; } if ((subLabel != nullptr) && !serviceEntry->CanAnswerSubType(subLabel)) { continue; } // Check for known-answer in this `RxMessage` and all its // related messages in case it is multi-packet query. for (const RxMessage *rxMessage = this; rxMessage != nullptr; rxMessage = rxMessage->GetNext()) { if (rxMessage->ShouldSuppressKnownAnswer(serviceType, subLabel, *serviceEntry)) { shouldSuppress = true; break; } } if (!shouldSuppress) { serviceEntry->AnswerServiceTypeQuestion(aInfo, subLabel); } } } bool Core::RxMessage::ShouldSuppressKnownAnswer(const Name &aServiceType, const char *aSubLabel, const ServiceEntry &aServiceEntry) const { bool shouldSuppress = false; uint16_t offset = mStartOffset[kAnswerSection]; uint16_t numRecords = mRecordCounts.GetFor(kAnswerSection); while (ResourceRecord::FindRecord(*mMessagePtr, offset, numRecords, aServiceType) == kErrorNone) { Error error; PtrRecord ptr; error = ResourceRecord::ReadRecord(*mMessagePtr, offset, ptr); if (error == kErrorNotFound) { // `ReadRecord()` will update the `offset` to skip over // the entire record if it does not match the expected // record type (PTR in this case). continue; } SuccessOrExit(error); // `offset` is now pointing to PTR name if (aServiceEntry.Matches(Name(*mMessagePtr, offset))) { shouldSuppress = aServiceEntry.ShouldSuppressKnownAnswer(ptr.GetTtl(), aSubLabel); ExitNow(); } // Parse the name and skip over it and update `offset` // to the start of the next record. SuccessOrExit(Name::ParseName(*mMessagePtr, offset)); } exit: return shouldSuppress; } bool Core::RxMessage::ParseQuestionNameAsSubType(const Question &aQuestion, Name::LabelBuffer &aSubLabel, Name &aServiceType) const { bool isSubType = false; uint16_t offset = aQuestion.mNameOffset; uint8_t length = sizeof(aSubLabel); SuccessOrExit(Name::ReadLabel(*mMessagePtr, offset, aSubLabel, length)); SuccessOrExit(Name::CompareLabel(*mMessagePtr, offset, kSubServiceLabel)); aServiceType.SetFromMessage(*mMessagePtr, offset); isSubType = true; exit: return isSubType; } void Core::RxMessage::AnswerAllServicesQuestion(const Question &aQuestion, const AnswerInfo &aInfo) { for (ServiceType &serviceType : Get().mServiceTypes) { bool shouldSuppress = false; // Check for known-answer in this `RxMessage` and all its // related messages in case it is multi-packet query. for (const RxMessage *rxMessage = this; rxMessage != nullptr; rxMessage = rxMessage->GetNext()) { if (rxMessage->ShouldSuppressKnownAnswer(aQuestion, serviceType)) { shouldSuppress = true; break; } } if (!shouldSuppress) { serviceType.AnswerQuestion(aInfo); } } } bool Core::RxMessage::ShouldSuppressKnownAnswer(const Question &aQuestion, const ServiceType &aServiceType) const { // Check answer section to determine whether to suppress answering // to "_services._dns-sd._udp" query with `aServiceType` bool shouldSuppress = false; uint16_t offset = mStartOffset[kAnswerSection]; uint16_t numRecords = mRecordCounts.GetFor(kAnswerSection); Name name(*mMessagePtr, aQuestion.mNameOffset); while (ResourceRecord::FindRecord(*mMessagePtr, offset, numRecords, name) == kErrorNone) { Error error; PtrRecord ptr; error = ResourceRecord::ReadRecord(*mMessagePtr, offset, ptr); if (error == kErrorNotFound) { // `ReadRecord()` will update the `offset` to skip over // the entire record if it does not match the expected // record type (PTR in this case). continue; } SuccessOrExit(error); // `offset` is now pointing to PTR name if (aServiceType.Matches(Name(*mMessagePtr, offset))) { shouldSuppress = aServiceType.ShouldSuppressKnownAnswer(ptr.GetTtl()); ExitNow(); } // Parse the name and skip over it and update `offset` // to the start of the next record. SuccessOrExit(Name::ParseName(*mMessagePtr, offset)); } exit: return shouldSuppress; } void Core::RxMessage::SendUnicastResponse(void) { TxMessage::Type responseType = mIsLegacyUnicast ? TxMessage::kLegacyUnicastResponse : TxMessage::kUnicastResponse; EntryContext context(GetInstance(), responseType, mSenderAddress, mIsLegacyUnicast ? mQueryId : 0); if (mIsLegacyUnicast) { // RFC6762, section 6.7: // Legacy Unicast Response must repeat the question context.mResponseMessage.AddQuestionFrom(*mMessagePtr); } for (HostEntry &entry : Get().mHostEntries) { entry.ClearAppendState(); entry.PrepareResponse(context); } for (ServiceEntry &entry : Get().mServiceEntries) { entry.ClearAppendState(); entry.PrepareResponse(context); } for (ServiceType &serviceType : Get().mServiceTypes) { serviceType.ClearAppendState(); serviceType.PrepareResponse(context); } context.mResponseMessage.Send(); } void Core::RxMessage::ProcessResponse(void) { if (!IsSelfOriginating()) { IterateOnAllRecordsInResponse(&RxMessage::ProcessRecordForConflict); } // We process record types in a specific order to ensure correct // passive cache creation: First PTR records are processed, which // may create passive SRV/TXT cache entries for discovered // services. Next SRV records are processed which may create TXT // cache entries for service names and IPv6 address cache entries // for associated host name. if (!Get().mBrowseCacheList.IsEmpty()) { IterateOnAllRecordsInResponse(&RxMessage::ProcessPtrRecord); } if (!Get().mSrvCacheList.IsEmpty()) { IterateOnAllRecordsInResponse(&RxMessage::ProcessSrvRecord); } if (!Get().mTxtCacheList.IsEmpty()) { IterateOnAllRecordsInResponse(&RxMessage::ProcessTxtRecord); } if (!Get().mIp6AddrCacheList.IsEmpty()) { IterateOnAllRecordsInResponse(&RxMessage::ProcessAaaaRecord); for (Ip6AddrCache &addrCache : Get().mIp6AddrCacheList) { addrCache.CommitNewResponseEntries(); } } if (!Get().mIp4AddrCacheList.IsEmpty()) { IterateOnAllRecordsInResponse(&RxMessage::ProcessARecord); for (Ip4AddrCache &addrCache : Get().mIp4AddrCacheList) { addrCache.CommitNewResponseEntries(); } } } void Core::RxMessage::IterateOnAllRecordsInResponse(RecordProcessor aRecordProcessor) { // Iterates over all records in the response, calling // `aRecordProcessor` for each. static const Section kSections[] = {kAnswerSection, kAdditionalDataSection}; for (Section section : kSections) { uint16_t offset = mStartOffset[section]; for (uint16_t numRecords = mRecordCounts.GetFor(section); numRecords > 0; numRecords--) { Name name(*mMessagePtr, offset); ResourceRecord record; IgnoreError(Name::ParseName(*mMessagePtr, offset)); IgnoreError(mMessagePtr->Read(offset, record)); if (!RrClassIsInternetOrAny(record.GetClass())) { continue; } (this->*aRecordProcessor)(name, record, offset); offset += static_cast(record.GetSize()); } } } void Core::RxMessage::ProcessRecordForConflict(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset) { HostEntry *hostEntry; ServiceEntry *serviceEntry; VerifyOrExit(aRecord.GetTtl() > 0); hostEntry = Get().mHostEntries.FindMatching(aName); if (hostEntry != nullptr) { hostEntry->HandleConflict(); } serviceEntry = Get().mServiceEntries.FindMatching(aName); if (serviceEntry != nullptr) { serviceEntry->HandleConflict(); } exit: OT_UNUSED_VARIABLE(aRecordOffset); } void Core::RxMessage::ProcessPtrRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset) { BrowseCache *browseCache; VerifyOrExit(aRecord.GetType() == ResourceRecord::kTypePtr); browseCache = Get().mBrowseCacheList.FindMatching(aName); VerifyOrExit(browseCache != nullptr); browseCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset); exit: return; } void Core::RxMessage::ProcessSrvRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset) { SrvCache *srvCache; VerifyOrExit(aRecord.GetType() == ResourceRecord::kTypeSrv); srvCache = Get().mSrvCacheList.FindMatching(aName); VerifyOrExit(srvCache != nullptr); srvCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset); exit: return; } void Core::RxMessage::ProcessTxtRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset) { TxtCache *txtCache; VerifyOrExit(aRecord.GetType() == ResourceRecord::kTypeTxt); txtCache = Get().mTxtCacheList.FindMatching(aName); VerifyOrExit(txtCache != nullptr); txtCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset); exit: return; } void Core::RxMessage::ProcessAaaaRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset) { Ip6AddrCache *ip6AddrCache; VerifyOrExit(aRecord.GetType() == ResourceRecord::kTypeAaaa); ip6AddrCache = Get().mIp6AddrCacheList.FindMatching(aName); VerifyOrExit(ip6AddrCache != nullptr); ip6AddrCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset); exit: return; } void Core::RxMessage::ProcessARecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset) { Ip4AddrCache *ip4AddrCache; VerifyOrExit(aRecord.GetType() == ResourceRecord::kTypeA); ip4AddrCache = Get().mIp4AddrCacheList.FindMatching(aName); VerifyOrExit(ip4AddrCache != nullptr); ip4AddrCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset); exit: return; } //--------------------------------------------------------------------------------------------------------------------- // Core::RxMessage::Question void Core::RxMessage::Question::ClearProcessState(void) { mCanAnswer = false; mIsUnique = false; mIsForService = false; mIsServiceType = false; mIsForAllServicesDnssd = false; mEntry = nullptr; } //--------------------------------------------------------------------------------------------------------------------- // Core::MultiPacketRxMessages Core::MultiPacketRxMessages::MultiPacketRxMessages(Instance &aInstance) : InstanceLocator(aInstance) , mTimer(aInstance) { } void Core::MultiPacketRxMessages::AddToExisting(OwnedPtr &aRxMessagePtr) { RxMsgEntry *msgEntry = mRxMsgEntries.FindMatching(aRxMessagePtr->GetSenderAddress()); VerifyOrExit(msgEntry != nullptr); msgEntry->Add(aRxMessagePtr); exit: return; } void Core::MultiPacketRxMessages::AddNew(OwnedPtr &aRxMessagePtr) { RxMsgEntry *newEntry = RxMsgEntry::Allocate(GetInstance()); OT_ASSERT(newEntry != nullptr); newEntry->Add(aRxMessagePtr); // First remove an existing entries matching same sender // before adding the new entry to the list. mRxMsgEntries.RemoveMatching(aRxMessagePtr->GetSenderAddress()); mRxMsgEntries.Push(*newEntry); } void Core::MultiPacketRxMessages::HandleTimer(void) { NextFireTime nextTime; OwningList expiredEntries; mRxMsgEntries.RemoveAllMatching(expiredEntries, ExpireChecker(nextTime.GetNow())); for (RxMsgEntry &expiredEntry : expiredEntries) { expiredEntry.mRxMessages.GetHead()->ProcessQuery(/* aShouldProcessTruncated */ true); } for (const RxMsgEntry &msgEntry : mRxMsgEntries) { nextTime.UpdateIfEarlier(msgEntry.mProcessTime); } mTimer.FireAtIfEarlier(nextTime); } void Core::MultiPacketRxMessages::Clear(void) { mTimer.Stop(); mRxMsgEntries.Clear(); } //--------------------------------------------------------------------------------------------------------------------- // Core::MultiPacketRxMessage::RxMsgEntry Core::MultiPacketRxMessages::RxMsgEntry::RxMsgEntry(Instance &aInstance) : InstanceLocator(aInstance) , mNext(nullptr) { } bool Core::MultiPacketRxMessages::RxMsgEntry::Matches(const AddressInfo &aAddress) const { bool matches = false; VerifyOrExit(!mRxMessages.IsEmpty()); matches = (mRxMessages.GetHead()->GetSenderAddress() == aAddress); exit: return matches; } bool Core::MultiPacketRxMessages::RxMsgEntry::Matches(const ExpireChecker &aExpireChecker) const { return (mProcessTime <= aExpireChecker.mNow); } void Core::MultiPacketRxMessages::RxMsgEntry::Add(OwnedPtr &aRxMessagePtr) { uint16_t numMsgs = 0; for (const RxMessage &rxMsg : mRxMessages) { // If a subsequent received `RxMessage` is also marked as // truncated, we again delay the process time. To avoid // continuous delay and piling up of messages in the list, // we limit the number of messages. numMsgs++; VerifyOrExit(numMsgs < kMaxNumMessages); OT_UNUSED_VARIABLE(rxMsg); } mProcessTime = TimerMilli::GetNow(); if (aRxMessagePtr->IsTruncated()) { mProcessTime += Random::NonCrypto::GetUint32InRange(kMinProcessDelay, kMaxProcessDelay); } // We push the new `RxMessage` at tail of the list to keep the // first query containing questions at the head of the list. mRxMessages.PushAfterTail(*aRxMessagePtr.Release()); Get().mMultiPacketRxMessages.mTimer.FireAtIfEarlier(mProcessTime); exit: return; } //--------------------------------------------------------------------------------------------------------------------- // Core::TxMessageHistory Core::TxMessageHistory::TxMessageHistory(Instance &aInstance) : InstanceLocator(aInstance) , mTimer(aInstance) { } void Core::TxMessageHistory::Clear(void) { mMsgEntries.Clear(); mTimer.Stop(); } void Core::TxMessageHistory::Add(const Message &aMessage) { MsgInfo info; MsgEntry *entry; info.InitFrom(aMessage); entry = mMsgEntries.FindMatching(info); if (entry == nullptr) { entry = MsgEntry::Allocate(); OT_ASSERT(entry != nullptr); entry->mInfo = info; mMsgEntries.Push(*entry); } entry->mExpireTime = TimerMilli::GetNow() + kExpireInterval; mTimer.FireAtIfEarlier(entry->mExpireTime); } bool Core::TxMessageHistory::Contains(const Message &aMessage) const { MsgInfo info; info.InitFrom(aMessage); return mMsgEntries.ContainsMatching(info); } void Core::TxMessageHistory::MsgInfo::InitFrom(const Message &aMessage) { OffsetRange offsetRange; offsetRange.InitFromMessageFullLength(aMessage); Clear(); mMsgLength = aMessage.GetLength(); mCrc16 = CrcCalculator(kCrc16AnsiPolynomial).Feed(aMessage, offsetRange); mCrc32 = CrcCalculator(kCrc32AnsiPolynomial).Feed(aMessage, offsetRange); } void Core::TxMessageHistory::HandleTimer(void) { NextFireTime nextTime; mMsgEntries.RemoveAndFreeAllMatching(ExpireChecker(nextTime.GetNow())); for (const MsgEntry &entry : mMsgEntries) { nextTime.UpdateIfEarlier(entry.mExpireTime); } mTimer.FireAtIfEarlier(nextTime); } template Error Core::Start(const BrowserResolverType &aBrowserOrResolver) { Error error = kErrorNone; CacheType *cacheEntry; VerifyOrExit(mIsEnabled, error = kErrorInvalidState); VerifyOrExit(aBrowserOrResolver.mCallback != nullptr, error = kErrorInvalidArgs); cacheEntry = GetCacheList().FindMatching(aBrowserOrResolver); if (cacheEntry == nullptr) { cacheEntry = CacheType::AllocateAndInit(GetInstance(), aBrowserOrResolver); OT_ASSERT(cacheEntry != nullptr); GetCacheList().Push(*cacheEntry); } error = cacheEntry->Add(aBrowserOrResolver); exit: return error; } template Error Core::Stop(const BrowserResolverType &aBrowserOrResolver) { Error error = kErrorNone; CacheType *cacheEntry; VerifyOrExit(mIsEnabled, error = kErrorInvalidState); VerifyOrExit(aBrowserOrResolver.mCallback != nullptr, error = kErrorInvalidArgs); cacheEntry = GetCacheList().FindMatching(aBrowserOrResolver); VerifyOrExit(cacheEntry != nullptr); cacheEntry->Remove(aBrowserOrResolver); exit: return error; } Error Core::StartBrowser(const Browser &aBrowser) { return Start(aBrowser); } Error Core::StopBrowser(const Browser &aBrowser) { return Stop(aBrowser); } Error Core::StartSrvResolver(const SrvResolver &aResolver) { return Start(aResolver); } Error Core::StopSrvResolver(const SrvResolver &aResolver) { return Stop(aResolver); } Error Core::StartTxtResolver(const TxtResolver &aResolver) { return Start(aResolver); } Error Core::StopTxtResolver(const TxtResolver &aResolver) { return Stop(aResolver); } Error Core::StartIp6AddressResolver(const AddressResolver &aResolver) { return Start(aResolver); } Error Core::StopIp6AddressResolver(const AddressResolver &aResolver) { return Stop(aResolver); } Error Core::StartIp4AddressResolver(const AddressResolver &aResolver) { return Start(aResolver); } Error Core::StopIp4AddressResolver(const AddressResolver &aResolver) { return Stop(aResolver); } void Core::AddPassiveSrvTxtCache(const char *aServiceInstance, const char *aServiceType) { ServiceName serviceName(aServiceInstance, aServiceType); if (!mSrvCacheList.ContainsMatching(serviceName)) { SrvCache *srvCache = SrvCache::AllocateAndInit(GetInstance(), serviceName); OT_ASSERT(srvCache != nullptr); mSrvCacheList.Push(*srvCache); } if (!mTxtCacheList.ContainsMatching(serviceName)) { TxtCache *txtCache = TxtCache::AllocateAndInit(GetInstance(), serviceName); OT_ASSERT(txtCache != nullptr); mTxtCacheList.Push(*txtCache); } } void Core::AddPassiveIp6AddrCache(const char *aHostName) { if (!mIp6AddrCacheList.ContainsMatching(aHostName)) { Ip6AddrCache *ip6AddrCache = Ip6AddrCache::AllocateAndInit(GetInstance(), aHostName); OT_ASSERT(ip6AddrCache != nullptr); mIp6AddrCacheList.Push(*ip6AddrCache); } } void Core::HandleCacheTimer(void) { CacheContext context(GetInstance()); ExpireChecker expireChecker(context.GetNow()); // First remove all expired entries. mBrowseCacheList.RemoveAndFreeAllMatching(expireChecker); mSrvCacheList.RemoveAndFreeAllMatching(expireChecker); mTxtCacheList.RemoveAndFreeAllMatching(expireChecker); mIp6AddrCacheList.RemoveAndFreeAllMatching(expireChecker); mIp4AddrCacheList.RemoveAndFreeAllMatching(expireChecker); // Process cache types in a specific order to optimize name // compression when constructing query messages. for (SrvCache &srvCache : mSrvCacheList) { srvCache.HandleTimer(context); } for (TxtCache &txtCache : mTxtCacheList) { txtCache.HandleTimer(context); } for (BrowseCache &browseCache : mBrowseCacheList) { browseCache.HandleTimer(context); } for (Ip6AddrCache &addrCache : mIp6AddrCacheList) { addrCache.HandleTimer(context); } for (Ip4AddrCache &addrCache : mIp4AddrCacheList) { addrCache.HandleTimer(context); } context.mQueryMessage.Send(); mCacheTimer.FireAtIfEarlier(context.mNextFireTime); } void Core::HandleCacheTask(void) { // `CacheTask` is used to remove empty/null callbacks // from cache entries. and also removing "passive" // cache entries that timed out. for (BrowseCache &browseCache : mBrowseCacheList) { browseCache.ClearEmptyCallbacks(); } for (SrvCache &srvCache : mSrvCacheList) { srvCache.ClearEmptyCallbacks(); } for (TxtCache &txtCache : mTxtCacheList) { txtCache.ClearEmptyCallbacks(); } for (Ip6AddrCache &addrCache : mIp6AddrCacheList) { addrCache.ClearEmptyCallbacks(); } for (Ip4AddrCache &addrCache : mIp4AddrCacheList) { addrCache.ClearEmptyCallbacks(); } } TimeMilli Core::RandomizeFirstProbeTxTime(void) { // Randomizes the transmission time of the first probe, adding a // delay between 20-250 msec. Subsequent probes within a short // window reuse the same delay for efficient aggregation. TimeMilli now = TimerMilli::GetNow(); // The comparison using `(mNextProbeTxTime - now)` will work // correctly even in the unlikely case that `now` has wrapped // (49 days has passed) since `mNextProbeTxTime` was last set. if ((mNextProbeTxTime - now) >= kMaxProbeDelay) { mNextProbeTxTime = now + Random::NonCrypto::GetUint32InRange(kMinProbeDelay, kMaxProbeDelay); } return mNextProbeTxTime; } TimeMilli Core::RandomizeInitialQueryTxTime(void) { TimeMilli now = TimerMilli::GetNow(); if ((mNextQueryTxTime - now) >= kMaxInitialQueryDelay) { mNextQueryTxTime = now + Random::NonCrypto::GetUint32InRange(kMinInitialQueryDelay, kMaxInitialQueryDelay); } return mNextQueryTxTime; } //--------------------------------------------------------------------------------------------------------------------- // Core::ResultCallback void Core::ResultCallback::Invoke(Instance &aInstance, const BrowseResult &aResult) const { if (mSharedCallback.mBrowse != nullptr) { mSharedCallback.mBrowse(&aInstance, &aResult); } } void Core::ResultCallback::Invoke(Instance &aInstance, const SrvResult &aResult) const { if (mSharedCallback.mSrv != nullptr) { mSharedCallback.mSrv(&aInstance, &aResult); } } void Core::ResultCallback::Invoke(Instance &aInstance, const TxtResult &aResult) const { if (mSharedCallback.mTxt != nullptr) { mSharedCallback.mTxt(&aInstance, &aResult); } } void Core::ResultCallback::Invoke(Instance &aInstance, const AddressResult &aResult) const { if (mSharedCallback.mAddress != nullptr) { mSharedCallback.mAddress(&aInstance, &aResult); } } //--------------------------------------------------------------------------------------------------------------------- // Core::CacheContext Core::CacheContext::CacheContext(Instance &aInstance) : mQueryMessage(aInstance, TxMessage::kMulticastQuery) { } //--------------------------------------------------------------------------------------------------------------------- // Core::CacheRecordInfo Core::CacheRecordInfo::CacheRecordInfo(void) : mTtl(0) , mQueryCount(0) { } bool Core::CacheRecordInfo::RefreshTtl(uint32_t aTtl) { // Called when cached record is refreshed. // Returns a boolean to indicate if TTL value // was changed or not. bool changed = (aTtl != mTtl); mLastRxTime = TimerMilli::GetNow(); mTtl = aTtl; mQueryCount = 0; return changed; } bool Core::CacheRecordInfo::ShouldExpire(TimeMilli aNow) const { return IsPresent() && (GetExpireTime() <= aNow); } void Core::CacheRecordInfo::UpdateStateAfterQuery(TimeMilli aNow) { VerifyOrExit(IsPresent()); // If the less than half TTL remains, then this record would not // be included as "Known-Answer" in the send query, so we can // count it towards queries to refresh this record. VerifyOrExit(LessThanHalfTtlRemains(aNow)); if (mQueryCount < kNumberOfQueries) { mQueryCount++; } exit: return; } void Core::CacheRecordInfo::UpdateQueryAndFireTimeOn(CacheEntry &aCacheEntry) { TimeMilli now; TimeMilli expireTime; VerifyOrExit(IsPresent()); now = TimerMilli::GetNow(); expireTime = GetExpireTime(); aCacheEntry.SetFireTime(expireTime); // Determine next query time for (uint8_t attemptIndex = mQueryCount; attemptIndex < kNumberOfQueries; attemptIndex++) { TimeMilli queryTime = GetQueryTime(attemptIndex); if (queryTime > now) { queryTime += Random::NonCrypto::GetUint32InRange(0, GetClampedTtl() * kQueryTtlVariation); aCacheEntry.ScheduleQuery(queryTime); break; } } exit: return; } bool Core::CacheRecordInfo::LessThanHalfTtlRemains(TimeMilli aNow) const { return IsPresent() && ((aNow - mLastRxTime) > TimeMilli::SecToMsec(GetClampedTtl()) / 2); } uint32_t Core::CacheRecordInfo::GetRemainingTtl(TimeMilli aNow) const { uint32_t remainingTtl = 0; TimeMilli expireTime; VerifyOrExit(IsPresent()); expireTime = GetExpireTime(); VerifyOrExit(aNow < expireTime); remainingTtl = TimeMilli::MsecToSec(expireTime - aNow); exit: return remainingTtl; } uint32_t Core::CacheRecordInfo::GetClampedTtl(void) const { // We clamp TTL to `kMaxTtl` (one day) to prevent `TimeMilli` // calculation overflow. return Min(mTtl, kMaxTtl); } TimeMilli Core::CacheRecordInfo::GetExpireTime(void) const { return mLastRxTime + TimeMilli::SecToMsec(GetClampedTtl()); } TimeMilli Core::CacheRecordInfo::GetQueryTime(uint8_t aAttemptIndex) const { // Queries are sent at 80%, 85%, 90% and 95% of TTL plus a random // variation of 2% (added when sceduling) static const uint32_t kTtlFactors[kNumberOfQueries] = { 80 * 1000 / 100, 85 * 1000 / 100, 90 * 1000 / 100, 95 * 1000 / 100, }; OT_ASSERT(aAttemptIndex < kNumberOfQueries); return mLastRxTime + kTtlFactors[aAttemptIndex] * GetClampedTtl(); } //--------------------------------------------------------------------------------------------------------------------- // Core::CacheEntry void Core::CacheEntry::Init(Instance &aInstance, Type aType) { InstanceLocatorInit::Init(aInstance); mType = aType; mInitalQueries = 0; mQueryPending = false; mLastQueryTimeValid = false; mIsActive = false; mDeleteTime = TimerMilli::GetNow() + kNonActiveDeleteTimeout; } void Core::CacheEntry::SetIsActive(bool aIsActive) { // Sets the active/passive state of a cache entry. An entry is // considered "active" when associated with at least one // resolver/browser. "Passive" entries (without a resolver/browser) // continue to process mDNS responses for updates but will not send // queries. Passive entries are deleted after `kNonActiveDeleteTimeout` // if no resolver/browser is added. mIsActive = aIsActive; if (!mIsActive) { mQueryPending = false; mDeleteTime = TimerMilli::GetNow() + kNonActiveDeleteTimeout; SetFireTime(mDeleteTime); } } bool Core::CacheEntry::ShouldDelete(TimeMilli aNow) const { return !mIsActive && (mDeleteTime <= aNow); } void Core::CacheEntry::StartInitialQueries(void) { mInitalQueries = 0; mLastQueryTimeValid = false; mLastQueryTime = Get().RandomizeInitialQueryTxTime(); ScheduleQuery(mLastQueryTime); } bool Core::CacheEntry::ShouldQuery(TimeMilli aNow) { return mQueryPending && (mNextQueryTime <= aNow); } void Core::CacheEntry::ScheduleQuery(TimeMilli aQueryTime) { VerifyOrExit(mIsActive); if (mQueryPending) { VerifyOrExit(aQueryTime < mNextQueryTime); } if (mLastQueryTimeValid) { aQueryTime = Max(aQueryTime, mLastQueryTime + kMinIntervalBetweenQueries); } mQueryPending = true; mNextQueryTime = aQueryTime; SetFireTime(mNextQueryTime); exit: return; } Error Core::CacheEntry::Add(const ResultCallback &aCallback) { Error error = kErrorNone; bool isFirst; ResultCallback *callback; callback = FindCallbackMatching(aCallback); VerifyOrExit(callback == nullptr, error = kErrorAlready); isFirst = mCallbacks.IsEmpty(); callback = ResultCallback::Allocate(aCallback); OT_ASSERT(callback != nullptr); mCallbacks.Push(*callback); // If this is the first active resolver/browser for this cache // entry, we mark it as active which allows queries to be sent We // decide whether or not to send initial queries (e.g., for // SRV/TXT cache entries we send initial queries if there is no // record, or less than half TTL remains). if (isFirst) { bool shouldStart = false; SetIsActive(true); switch (mType) { case kBrowseCache: shouldStart = true; break; case kSrvCache: case kTxtCache: shouldStart = As().ShouldStartInitialQueries(); break; case kIp6AddrCache: case kIp4AddrCache: shouldStart = As().ShouldStartInitialQueries(); break; } if (shouldStart) { StartInitialQueries(); } DetermineNextFireTime(); ScheduleTimer(); } // Report any discovered/cached result to the newly added // callback. switch (mType) { case kBrowseCache: As().ReportResultsTo(*callback); break; case kSrvCache: As().ReportResultTo(*callback); break; case kTxtCache: As().ReportResultTo(*callback); break; case kIp6AddrCache: case kIp4AddrCache: As().ReportResultsTo(*callback); break; } exit: return error; } void Core::CacheEntry::Remove(const ResultCallback &aCallback) { ResultCallback *callback = FindCallbackMatching(aCallback); VerifyOrExit(callback != nullptr); // We clear the callback setting it to `nullptr` without removing // it from the list here, since the `Remove()` method may be // called from a callback while we are iterating over the list. // Removal from the list is deferred to `mCacheTask` which will // later call `ClearEmptyCallbacks()`. callback->ClearCallback(); Get().mCacheTask.Post(); exit: return; } void Core::CacheEntry::ClearEmptyCallbacks(void) { mCallbacks.RemoveAndFreeAllMatching(EmptyChecker()); if (mCallbacks.IsEmpty()) { SetIsActive(false); DetermineNextFireTime(); ScheduleTimer(); } } void Core::CacheEntry::HandleTimer(CacheContext &aContext) { switch (mType) { case kBrowseCache: As().ClearCompressOffsets(); break; case kSrvCache: case kTxtCache: As().ClearCompressOffsets(); break; case kIp6AddrCache: case kIp4AddrCache: // `AddrCache` entries do not track any append state or // compress offset since the host name would not be used // in any other query question. break; } VerifyOrExit(HasFireTime()); VerifyOrExit(GetFireTime() <= aContext.GetNow()); ClearFireTime(); if (ShouldDelete(aContext.GetNow())) { ExitNow(); } if (ShouldQuery(aContext.GetNow())) { mQueryPending = false; PrepareQuery(aContext); } switch (mType) { case kBrowseCache: As().ProcessExpiredRecords(aContext.GetNow()); break; case kSrvCache: As().ProcessExpiredRecords(aContext.GetNow()); break; case kTxtCache: As().ProcessExpiredRecords(aContext.GetNow()); break; case kIp6AddrCache: case kIp4AddrCache: As().ProcessExpiredRecords(aContext.GetNow()); break; } DetermineNextFireTime(); exit: UpdateNextFireTimeOn(aContext.mNextFireTime); } Core::ResultCallback *Core::CacheEntry::FindCallbackMatching(const ResultCallback &aCallback) { ResultCallback *callback = nullptr; switch (mType) { case kBrowseCache: callback = mCallbacks.FindMatching(aCallback.mSharedCallback.mBrowse); break; case kSrvCache: callback = mCallbacks.FindMatching(aCallback.mSharedCallback.mSrv); break; case kTxtCache: callback = mCallbacks.FindMatching(aCallback.mSharedCallback.mTxt); break; case kIp6AddrCache: case kIp4AddrCache: callback = mCallbacks.FindMatching(aCallback.mSharedCallback.mAddress); break; } return callback; } void Core::CacheEntry::DetermineNextFireTime(void) { mQueryPending = false; if (mInitalQueries < kNumberOfInitalQueries) { uint32_t interval = (mInitalQueries == 0) ? 0 : (1U << (mInitalQueries - 1)) * kInitialQueryInterval; ScheduleQuery(mLastQueryTime + interval); } if (!mIsActive) { SetFireTime(mDeleteTime); } // Let each cache entry type schedule query and // fire times based on the state of its discovered // records. switch (mType) { case kBrowseCache: As().DetermineRecordFireTime(); break; case kSrvCache: case kTxtCache: As().DetermineRecordFireTime(); break; case kIp6AddrCache: case kIp4AddrCache: As().DetermineRecordFireTime(); break; } } void Core::CacheEntry::ScheduleTimer(void) { ScheduleFireTimeOn(Get().mCacheTimer); } void Core::CacheEntry::PrepareQuery(CacheContext &aContext) { bool prepareAgain = false; do { TxMessage &query = aContext.mQueryMessage; query.SaveCurrentState(); switch (mType) { case kBrowseCache: As().PreparePtrQuestion(query, aContext.GetNow()); break; case kSrvCache: As().PrepareSrvQuestion(query); break; case kTxtCache: As().PrepareTxtQuestion(query); break; case kIp6AddrCache: As().PrepareAaaaQuestion(query); break; case kIp4AddrCache: As().PrepareAQuestion(query); break; } query.CheckSizeLimitToPrepareAgain(prepareAgain); } while (prepareAgain); mLastQueryTimeValid = true; mLastQueryTime = aContext.GetNow(); if (mInitalQueries < kNumberOfInitalQueries) { mInitalQueries++; } // Let the cache entry super-classes update their state // after query was sent. switch (mType) { case kBrowseCache: As().UpdateRecordStateAfterQuery(aContext.GetNow()); break; case kSrvCache: case kTxtCache: As().UpdateRecordStateAfterQuery(aContext.GetNow()); break; case kIp6AddrCache: case kIp4AddrCache: As().UpdateRecordStateAfterQuery(aContext.GetNow()); break; } } template void Core::CacheEntry::InvokeCallbacks(const ResultType &aResult) { for (const ResultCallback &callback : mCallbacks) { callback.Invoke(GetInstance(), aResult); } } //--------------------------------------------------------------------------------------------------------------------- // Core::BrowseCache Error Core::BrowseCache::Init(Instance &aInstance, const char *aServiceType, const char *aSubTypeLabel) { Error error = kErrorNone; CacheEntry::Init(aInstance, kBrowseCache); mNext = nullptr; ClearCompressOffsets(); SuccessOrExit(error = mServiceType.Set(aServiceType)); SuccessOrExit(error = mSubTypeLabel.Set(aSubTypeLabel)); exit: return error; } Error Core::BrowseCache::Init(Instance &aInstance, const Browser &aBrowser) { return Init(aInstance, aBrowser.mServiceType, aBrowser.mSubTypeLabel); } void Core::BrowseCache::ClearCompressOffsets(void) { mServiceTypeOffset = kUnspecifiedOffset; mSubServiceTypeOffset = kUnspecifiedOffset; mSubServiceNameOffset = kUnspecifiedOffset; } bool Core::BrowseCache::Matches(const Name &aFullName) const { bool matches = false; bool isSubType = !mSubTypeLabel.IsNull(); Name name = aFullName; OT_ASSERT(name.IsFromMessage()); if (isSubType) { uint16_t offset; const Message &message = name.GetAsMessage(offset); SuccessOrExit(Name::CompareLabel(message, offset, mSubTypeLabel.AsCString())); name.SetFromMessage(message, offset); } matches = name.Matches(isSubType ? kSubServiceLabel : nullptr, mServiceType.AsCString(), kLocalDomain); exit: return matches; } bool Core::BrowseCache::Matches(const char *aServiceType, const char *aSubTypeLabel) const { bool matches = false; if (aSubTypeLabel == nullptr) { VerifyOrExit(mSubTypeLabel.IsNull()); } else { VerifyOrExit(NameMatch(mSubTypeLabel, aSubTypeLabel)); } matches = NameMatch(mServiceType, aServiceType); exit: return matches; } bool Core::BrowseCache::Matches(const Browser &aBrowser) const { return Matches(aBrowser.mServiceType, aBrowser.mSubTypeLabel); } bool Core::BrowseCache::Matches(const ExpireChecker &aExpireChecker) const { return ShouldDelete(aExpireChecker.mNow); } Error Core::BrowseCache::Add(const Browser &aBrowser) { return CacheEntry::Add(ResultCallback(aBrowser.mCallback)); } void Core::BrowseCache::Remove(const Browser &aBrowser) { CacheEntry::Remove(ResultCallback(aBrowser.mCallback)); } void Core::BrowseCache::ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset) { // Name and record type in `aMessage` are already matched. uint16_t offset = aRecordOffset; PtrRecord ptr; Name::Buffer fullServiceType; Name::Buffer serviceInstance; BrowseResult result; PtrEntry *ptrEntry; bool changed = false; // Read the PTR record. `ReadPtrName()` validates that // PTR record is well-formed. SuccessOrExit(aMessage.Read(offset, ptr)); offset += sizeof(ptr); SuccessOrExit(ptr.ReadPtrName(aMessage, offset, serviceInstance, fullServiceType)); VerifyOrExit(Name(fullServiceType).Matches(nullptr, mServiceType.AsCString(), kLocalDomain)); ptrEntry = mPtrEntries.FindMatching(serviceInstance); if (ptr.GetTtl() == 0) { VerifyOrExit((ptrEntry != nullptr) && ptrEntry->mRecord.IsPresent()); ptrEntry->mRecord.RefreshTtl(0); changed = true; } else { if (ptrEntry == nullptr) { ptrEntry = PtrEntry::AllocateAndInit(serviceInstance); VerifyOrExit(ptrEntry != nullptr); mPtrEntries.Push(*ptrEntry); } if (ptrEntry->mRecord.RefreshTtl(ptr.GetTtl())) { changed = true; } } VerifyOrExit(changed); if (ptrEntry->mRecord.IsPresent() && IsActive()) { Get().AddPassiveSrvTxtCache(ptrEntry->mServiceInstance.AsCString(), mServiceType.AsCString()); } ptrEntry->ConvertTo(result, *this); InvokeCallbacks(result); exit: DetermineNextFireTime(); ScheduleTimer(); } void Core::BrowseCache::PreparePtrQuestion(TxMessage &aQuery, TimeMilli aNow) { Question question; DiscoverCompressOffsets(); question.SetType(ResourceRecord::kTypePtr); question.SetClass(ResourceRecord::kClassInternet); AppendServiceTypeOrSubTypeTo(aQuery, kQuestionSection); SuccessOrAssert(aQuery.SelectMessageFor(kQuestionSection).Append(question)); aQuery.IncrementRecordCount(kQuestionSection); for (const PtrEntry &ptrEntry : mPtrEntries) { if (!ptrEntry.mRecord.IsPresent() || ptrEntry.mRecord.LessThanHalfTtlRemains(aNow)) { continue; } AppendKnownAnswer(aQuery, ptrEntry, aNow); } } void Core::BrowseCache::DiscoverCompressOffsets(void) { for (const BrowseCache &browseCache : Get().mBrowseCacheList) { if (&browseCache == this) { break; } if (NameMatch(browseCache.mServiceType, mServiceType)) { UpdateCompressOffset(mServiceTypeOffset, browseCache.mServiceTypeOffset); UpdateCompressOffset(mSubServiceTypeOffset, browseCache.mSubServiceTypeOffset); VerifyOrExit(mSubServiceTypeOffset == kUnspecifiedOffset); } } VerifyOrExit(mServiceTypeOffset == kUnspecifiedOffset); for (const SrvCache &srvCache : Get().mSrvCacheList) { if (NameMatch(srvCache.mServiceType, mServiceType)) { UpdateCompressOffset(mServiceTypeOffset, srvCache.mServiceTypeOffset); VerifyOrExit(mServiceTypeOffset == kUnspecifiedOffset); } } for (const TxtCache &txtCache : Get().mTxtCacheList) { if (NameMatch(txtCache.mServiceType, mServiceType)) { UpdateCompressOffset(mServiceTypeOffset, txtCache.mServiceTypeOffset); VerifyOrExit(mServiceTypeOffset == kUnspecifiedOffset); } } exit: return; } void Core::BrowseCache::AppendServiceTypeOrSubTypeTo(TxMessage &aTxMessage, Section aSection) { if (!mSubTypeLabel.IsNull()) { AppendOutcome outcome; outcome = aTxMessage.AppendLabel(aSection, mSubTypeLabel.AsCString(), mSubServiceNameOffset); VerifyOrExit(outcome != kAppendedFullNameAsCompressed); outcome = aTxMessage.AppendLabel(aSection, kSubServiceLabel, mSubServiceTypeOffset); VerifyOrExit(outcome != kAppendedFullNameAsCompressed); } aTxMessage.AppendServiceType(aSection, mServiceType.AsCString(), mServiceTypeOffset); exit: return; } void Core::BrowseCache::AppendKnownAnswer(TxMessage &aTxMessage, const PtrEntry &aPtrEntry, TimeMilli aNow) { Message &message = aTxMessage.SelectMessageFor(kAnswerSection); PtrRecord ptr; uint16_t offset; ptr.Init(); ptr.SetTtl(aPtrEntry.mRecord.GetRemainingTtl(aNow)); AppendServiceTypeOrSubTypeTo(aTxMessage, kAnswerSection); offset = message.GetLength(); SuccessOrAssert(message.Append(ptr)); SuccessOrAssert(Name::AppendLabel(aPtrEntry.mServiceInstance.AsCString(), message)); aTxMessage.AppendServiceType(kAnswerSection, mServiceType.AsCString(), mServiceTypeOffset); UpdateRecordLengthInMessage(ptr, message, offset); aTxMessage.IncrementRecordCount(kAnswerSection); } void Core::BrowseCache::UpdateRecordStateAfterQuery(TimeMilli aNow) { for (PtrEntry &ptrEntry : mPtrEntries) { ptrEntry.mRecord.UpdateStateAfterQuery(aNow); } } void Core::BrowseCache::DetermineRecordFireTime(void) { for (PtrEntry &ptrEntry : mPtrEntries) { ptrEntry.mRecord.UpdateQueryAndFireTimeOn(*this); } } void Core::BrowseCache::ProcessExpiredRecords(TimeMilli aNow) { OwningList expiredEntries; mPtrEntries.RemoveAllMatching(expiredEntries, ExpireChecker(aNow)); for (PtrEntry &exiredEntry : expiredEntries) { BrowseResult result; exiredEntry.mRecord.RefreshTtl(0); exiredEntry.ConvertTo(result, *this); InvokeCallbacks(result); } } void Core::BrowseCache::ReportResultsTo(ResultCallback &aCallback) const { for (const PtrEntry &ptrEntry : mPtrEntries) { if (ptrEntry.mRecord.IsPresent()) { BrowseResult result; ptrEntry.ConvertTo(result, *this); aCallback.Invoke(GetInstance(), result); } } } #if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE void Core::BrowseCache::CopyInfoTo(Browser &aBrowser, CacheInfo &aInfo) const { aBrowser.mServiceType = mServiceType.AsCString(); aBrowser.mSubTypeLabel = mSubTypeLabel.AsCString(); aBrowser.mInfraIfIndex = Get().mInfraIfIndex; aBrowser.mCallback = nullptr; aInfo.mIsActive = IsActive(); aInfo.mHasCachedResults = !mPtrEntries.IsEmpty(); } #endif //--------------------------------------------------------------------------------------------------------------------- // Core::BrowseCache::PtrEntry Error Core::BrowseCache::PtrEntry::Init(const char *aServiceInstance) { mNext = nullptr; return mServiceInstance.Set(aServiceInstance); } bool Core::BrowseCache::PtrEntry::Matches(const ExpireChecker &aExpireChecker) const { return mRecord.ShouldExpire(aExpireChecker.mNow); } void Core::BrowseCache::PtrEntry::ConvertTo(BrowseResult &aResult, const BrowseCache &aBrowseCache) const { ClearAllBytes(aResult); aResult.mServiceType = aBrowseCache.mServiceType.AsCString(); aResult.mSubTypeLabel = aBrowseCache.mSubTypeLabel.AsCString(); aResult.mServiceInstance = mServiceInstance.AsCString(); aResult.mTtl = mRecord.GetTtl(); aResult.mInfraIfIndex = aBrowseCache.Get().mInfraIfIndex; } //--------------------------------------------------------------------------------------------------------------------- // Core::ServiceCache Error Core::ServiceCache::Init(Instance &aInstance, Type aType, const char *aServiceInstance, const char *aServiceType) { Error error = kErrorNone; CacheEntry::Init(aInstance, aType); ClearCompressOffsets(); SuccessOrExit(error = mServiceInstance.Set(aServiceInstance)); SuccessOrExit(error = mServiceType.Set(aServiceType)); exit: return error; } void Core::ServiceCache::ClearCompressOffsets(void) { mServiceNameOffset = kUnspecifiedOffset; mServiceTypeOffset = kUnspecifiedOffset; } bool Core::ServiceCache::Matches(const Name &aFullName) const { return aFullName.Matches(mServiceInstance.AsCString(), mServiceType.AsCString(), kLocalDomain); } bool Core::ServiceCache::Matches(const char *aServiceInstance, const char *aServiceType) const { return NameMatch(mServiceInstance, aServiceInstance) && NameMatch(mServiceType, aServiceType); } void Core::ServiceCache::PrepareQueryQuestion(TxMessage &aQuery, uint16_t aRrType) { Message &message = aQuery.SelectMessageFor(kQuestionSection); Question question; question.SetType(aRrType); question.SetClass(ResourceRecord::kClassInternet); AppendServiceNameTo(aQuery, kQuestionSection); SuccessOrAssert(message.Append(question)); aQuery.IncrementRecordCount(kQuestionSection); } void Core::ServiceCache::AppendServiceNameTo(TxMessage &aTxMessage, Section aSection) { AppendOutcome outcome; outcome = aTxMessage.AppendLabel(aSection, mServiceInstance.AsCString(), mServiceNameOffset); VerifyOrExit(outcome != kAppendedFullNameAsCompressed); aTxMessage.AppendServiceType(aSection, mServiceType.AsCString(), mServiceTypeOffset); exit: return; } void Core::ServiceCache::UpdateRecordStateAfterQuery(TimeMilli aNow) { mRecord.UpdateStateAfterQuery(aNow); } void Core::ServiceCache::DetermineRecordFireTime(void) { mRecord.UpdateQueryAndFireTimeOn(*this); } bool Core::ServiceCache::ShouldStartInitialQueries(void) const { // This is called when the first active resolver is added // for this cache entry to determine whether we should // send initial queries. It is possible that we were passively // monitoring and have some cached record for this entry. // We send initial queries if there is no record or less than // half of the original TTL remains. return !mRecord.IsPresent() || mRecord.LessThanHalfTtlRemains(TimerMilli::GetNow()); } //--------------------------------------------------------------------------------------------------------------------- // Core::SrvCache Error Core::SrvCache::Init(Instance &aInstance, const char *aServiceInstance, const char *aServiceType) { mNext = nullptr; mPort = 0; mPriority = 0; mWeight = 0; return ServiceCache::Init(aInstance, kSrvCache, aServiceInstance, aServiceType); } Error Core::SrvCache::Init(Instance &aInstance, const ServiceName &aServiceName) { return Init(aInstance, aServiceName.mServiceInstance, aServiceName.mServiceType); } Error Core::SrvCache::Init(Instance &aInstance, const SrvResolver &aResolver) { return Init(aInstance, aResolver.mServiceInstance, aResolver.mServiceType); } bool Core::SrvCache::Matches(const Name &aFullName) const { return ServiceCache::Matches(aFullName); } bool Core::SrvCache::Matches(const ServiceName &aServiceName) const { return ServiceCache::Matches(aServiceName.mServiceInstance, aServiceName.mServiceType); } bool Core::SrvCache::Matches(const SrvResolver &aResolver) const { return ServiceCache::Matches(aResolver.mServiceInstance, aResolver.mServiceType); } bool Core::SrvCache::Matches(const ExpireChecker &aExpireChecker) const { return ShouldDelete(aExpireChecker.mNow); } Error Core::SrvCache::Add(const SrvResolver &aResolver) { return CacheEntry::Add(ResultCallback(aResolver.mCallback)); } void Core::SrvCache::Remove(const SrvResolver &aResolver) { CacheEntry::Remove(ResultCallback(aResolver.mCallback)); } void Core::SrvCache::ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset) { // Name and record type in `aMessage` are already matched. uint16_t offset = aRecordOffset; SrvRecord srv; Name::Buffer hostFullName; Name::Buffer hostName; SrvResult result; bool changed = false; // Read the SRV record. `ReadTargetHostName()` validates that // SRV record is well-formed. SuccessOrExit(aMessage.Read(offset, srv)); offset += sizeof(srv); SuccessOrExit(srv.ReadTargetHostName(aMessage, offset, hostFullName)); SuccessOrExit(Name::ExtractLabels(hostFullName, kLocalDomain, hostName)); if (srv.GetTtl() == 0) { VerifyOrExit(mRecord.IsPresent()); mHostName.Free(); mRecord.RefreshTtl(0); changed = true; } else { if (!mRecord.IsPresent() || !NameMatch(mHostName, hostName)) { SuccessOrAssert(mHostName.Set(hostName)); changed = true; } if (!mRecord.IsPresent() || (mPort != srv.GetPort())) { mPort = srv.GetPort(); changed = true; } if (!mRecord.IsPresent() || (mPriority != srv.GetPriority())) { mPriority = srv.GetPriority(); changed = true; } if (!mRecord.IsPresent() || (mWeight != srv.GetWeight())) { mWeight = srv.GetWeight(); changed = true; } if (mRecord.RefreshTtl(srv.GetTtl())) { changed = true; } } VerifyOrExit(changed); if (mRecord.IsPresent()) { StopInitialQueries(); // If not present already, we add a passive `TxtCache` for the // same service name, and an `Ip6AddrCache` for the host name. Get().AddPassiveSrvTxtCache(mServiceInstance.AsCString(), mServiceType.AsCString()); Get().AddPassiveIp6AddrCache(mHostName.AsCString()); } ConvertTo(result); InvokeCallbacks(result); exit: DetermineNextFireTime(); ScheduleTimer(); } void Core::SrvCache::PrepareSrvQuestion(TxMessage &aQuery) { DiscoverCompressOffsets(); PrepareQueryQuestion(aQuery, ResourceRecord::kTypeSrv); } void Core::SrvCache::DiscoverCompressOffsets(void) { for (const SrvCache &srvCache : Get().mSrvCacheList) { if (&srvCache == this) { break; } if (NameMatch(srvCache.mServiceType, mServiceType)) { UpdateCompressOffset(mServiceTypeOffset, srvCache.mServiceTypeOffset); } if (mServiceTypeOffset != kUnspecifiedOffset) { break; } } } void Core::SrvCache::ProcessExpiredRecords(TimeMilli aNow) { if (mRecord.ShouldExpire(aNow)) { SrvResult result; mRecord.RefreshTtl(0); ConvertTo(result); InvokeCallbacks(result); } } void Core::SrvCache::ReportResultTo(ResultCallback &aCallback) const { if (mRecord.IsPresent()) { SrvResult result; ConvertTo(result); aCallback.Invoke(GetInstance(), result); } } void Core::SrvCache::ConvertTo(SrvResult &aResult) const { ClearAllBytes(aResult); aResult.mServiceInstance = mServiceInstance.AsCString(); aResult.mServiceType = mServiceType.AsCString(); aResult.mHostName = mHostName.AsCString(); aResult.mPort = mPort; aResult.mPriority = mPriority; aResult.mWeight = mWeight; aResult.mTtl = mRecord.GetTtl(); aResult.mInfraIfIndex = Get().mInfraIfIndex; } #if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE void Core::SrvCache::CopyInfoTo(SrvResolver &aResolver, CacheInfo &aInfo) const { aResolver.mServiceInstance = mServiceInstance.AsCString(); aResolver.mServiceType = mServiceType.AsCString(); aResolver.mInfraIfIndex = Get().mInfraIfIndex; aResolver.mCallback = nullptr; aInfo.mIsActive = IsActive(); aInfo.mHasCachedResults = mRecord.IsPresent(); } #endif //--------------------------------------------------------------------------------------------------------------------- // Core::TxtCache Error Core::TxtCache::Init(Instance &aInstance, const char *aServiceInstance, const char *aServiceType) { mNext = nullptr; return ServiceCache::Init(aInstance, kTxtCache, aServiceInstance, aServiceType); } Error Core::TxtCache::Init(Instance &aInstance, const ServiceName &aServiceName) { return Init(aInstance, aServiceName.mServiceInstance, aServiceName.mServiceType); } Error Core::TxtCache::Init(Instance &aInstance, const TxtResolver &aResolver) { return Init(aInstance, aResolver.mServiceInstance, aResolver.mServiceType); } bool Core::TxtCache::Matches(const Name &aFullName) const { return ServiceCache::Matches(aFullName); } bool Core::TxtCache::Matches(const ServiceName &aServiceName) const { return ServiceCache::Matches(aServiceName.mServiceInstance, aServiceName.mServiceType); } bool Core::TxtCache::Matches(const TxtResolver &aResolver) const { return ServiceCache::Matches(aResolver.mServiceInstance, aResolver.mServiceType); } bool Core::TxtCache::Matches(const ExpireChecker &aExpireChecker) const { return ShouldDelete(aExpireChecker.mNow); } Error Core::TxtCache::Add(const TxtResolver &aResolver) { return CacheEntry::Add(ResultCallback(aResolver.mCallback)); } void Core::TxtCache::Remove(const TxtResolver &aResolver) { CacheEntry::Remove(ResultCallback(aResolver.mCallback)); } void Core::TxtCache::ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset) { // Name and record type are already matched. uint16_t offset = aRecordOffset; TxtRecord txt; TxtResult result; bool changed = false; SuccessOrExit(aMessage.Read(offset, txt)); offset += sizeof(txt); if (txt.GetTtl() == 0) { VerifyOrExit(mRecord.IsPresent()); mTxtData.Free(); mRecord.RefreshTtl(0); changed = true; } else { VerifyOrExit(txt.GetLength() > 0); VerifyOrExit(aMessage.GetLength() >= offset + txt.GetLength()); if (!mRecord.IsPresent() || (mTxtData.GetLength() != txt.GetLength()) || !aMessage.CompareBytes(offset, mTxtData.GetBytes(), mTxtData.GetLength())) { SuccessOrAssert(mTxtData.SetFrom(aMessage, offset, txt.GetLength())); changed = true; } if (mRecord.RefreshTtl(txt.GetTtl())) { changed = true; } } VerifyOrExit(changed); if (mRecord.IsPresent()) { StopInitialQueries(); } ConvertTo(result); InvokeCallbacks(result); exit: DetermineNextFireTime(); ScheduleTimer(); } void Core::TxtCache::PrepareTxtQuestion(TxMessage &aQuery) { DiscoverCompressOffsets(); PrepareQueryQuestion(aQuery, ResourceRecord::kTypeTxt); } void Core::TxtCache::DiscoverCompressOffsets(void) { for (const SrvCache &srvCache : Get().mSrvCacheList) { if (!NameMatch(srvCache.mServiceType, mServiceType)) { continue; } UpdateCompressOffset(mServiceTypeOffset, srvCache.mServiceTypeOffset); if (NameMatch(srvCache.mServiceInstance, mServiceInstance)) { UpdateCompressOffset(mServiceNameOffset, srvCache.mServiceNameOffset); } VerifyOrExit(mServiceNameOffset == kUnspecifiedOffset); } for (const TxtCache &txtCache : Get().mTxtCacheList) { if (&txtCache == this) { break; } if (NameMatch(txtCache.mServiceType, mServiceType)) { UpdateCompressOffset(mServiceTypeOffset, txtCache.mServiceTypeOffset); } VerifyOrExit(mServiceTypeOffset == kUnspecifiedOffset); } exit: return; } void Core::TxtCache::ProcessExpiredRecords(TimeMilli aNow) { if (mRecord.ShouldExpire(aNow)) { TxtResult result; mRecord.RefreshTtl(0); ConvertTo(result); InvokeCallbacks(result); } } void Core::TxtCache::ReportResultTo(ResultCallback &aCallback) const { if (mRecord.IsPresent()) { TxtResult result; ConvertTo(result); aCallback.Invoke(GetInstance(), result); } } void Core::TxtCache::ConvertTo(TxtResult &aResult) const { ClearAllBytes(aResult); aResult.mServiceInstance = mServiceInstance.AsCString(); aResult.mServiceType = mServiceType.AsCString(); aResult.mTxtData = mTxtData.GetBytes(); aResult.mTxtDataLength = mTxtData.GetLength(); aResult.mTtl = mRecord.GetTtl(); aResult.mInfraIfIndex = Get().mInfraIfIndex; } #if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE void Core::TxtCache::CopyInfoTo(TxtResolver &aResolver, CacheInfo &aInfo) const { aResolver.mServiceInstance = mServiceInstance.AsCString(); aResolver.mServiceType = mServiceType.AsCString(); aResolver.mInfraIfIndex = Get().mInfraIfIndex; aResolver.mCallback = nullptr; aInfo.mIsActive = IsActive(); aInfo.mHasCachedResults = mRecord.IsPresent(); } #endif //--------------------------------------------------------------------------------------------------------------------- // Core::AddrCache Error Core::AddrCache::Init(Instance &aInstance, Type aType, const char *aHostName) { CacheEntry::Init(aInstance, aType); mNext = nullptr; mShouldFlush = false; return mName.Set(aHostName); } Error Core::AddrCache::Init(Instance &aInstance, Type aType, const AddressResolver &aResolver) { return Init(aInstance, aType, aResolver.mHostName); } bool Core::AddrCache::Matches(const Name &aFullName) const { return aFullName.Matches(nullptr, mName.AsCString(), kLocalDomain); } bool Core::AddrCache::Matches(const char *aName) const { return NameMatch(mName, aName); } bool Core::AddrCache::Matches(const AddressResolver &aResolver) const { return Matches(aResolver.mHostName); } bool Core::AddrCache::Matches(const ExpireChecker &aExpireChecker) const { return ShouldDelete(aExpireChecker.mNow); } Error Core::AddrCache::Add(const AddressResolver &aResolver) { return CacheEntry::Add(ResultCallback(aResolver.mCallback)); } void Core::AddrCache::Remove(const AddressResolver &aResolver) { CacheEntry::Remove(ResultCallback(aResolver.mCallback)); } void Core::AddrCache::PrepareQueryQuestion(TxMessage &aQuery, uint16_t aRrType) { Question question; question.SetType(aRrType); question.SetClass(ResourceRecord::kClassInternet); AppendNameTo(aQuery, kQuestionSection); SuccessOrAssert(aQuery.SelectMessageFor(kQuestionSection).Append(question)); aQuery.IncrementRecordCount(kQuestionSection); } void Core::AddrCache::AppendNameTo(TxMessage &aTxMessage, Section aSection) { uint16_t compressOffset = kUnspecifiedOffset; AppendOutcome outcome; outcome = aTxMessage.AppendMultipleLabels(aSection, mName.AsCString(), compressOffset); VerifyOrExit(outcome != kAppendedFullNameAsCompressed); aTxMessage.AppendDomainName(aSection); exit: return; } void Core::AddrCache::UpdateRecordStateAfterQuery(TimeMilli aNow) { for (AddrEntry &entry : mCommittedEntries) { entry.mRecord.UpdateStateAfterQuery(aNow); } } void Core::AddrCache::DetermineRecordFireTime(void) { for (AddrEntry &entry : mCommittedEntries) { entry.mRecord.UpdateQueryAndFireTimeOn(*this); } } void Core::AddrCache::ProcessExpiredRecords(TimeMilli aNow) { Heap::Array addrArray; AddressResult result; bool didRemoveAny; didRemoveAny = mCommittedEntries.RemoveAndFreeAllMatching(ExpireChecker(aNow)); VerifyOrExit(didRemoveAny); ConstructResult(result, addrArray); InvokeCallbacks(result); exit: return; } void Core::AddrCache::ReportResultsTo(ResultCallback &aCallback) const { Heap::Array addrArray; AddressResult result; ConstructResult(result, addrArray); if (result.mAddressesLength > 0) { aCallback.Invoke(GetInstance(), result); } } void Core::AddrCache::ConstructResult(AddressResult &aResult, Heap::Array &aAddrArray) const { // Prepares an `AddressResult` populating it with discovered // addresses from the `AddrCache` entry. Uses a caller-provided // `Heap::Array` reference (`aAddrArray`) to ensure that the // allocated array for `aResult.mAddresses` remains valid until // after the `aResult` is used (passed as input to // `ResultCallback`). uint16_t addrCount = 0; ClearAllBytes(aResult); aAddrArray.Free(); for (const AddrEntry &entry : mCommittedEntries) { if (entry.mRecord.IsPresent()) { addrCount++; } } if (addrCount > 0) { SuccessOrAssert(aAddrArray.ReserveCapacity(addrCount)); for (const AddrEntry &entry : mCommittedEntries) { AddressAndTtl *addr; if (!entry.mRecord.IsPresent()) { continue; } addr = aAddrArray.PushBack(); OT_ASSERT(addr != nullptr); addr->mAddress = entry.mAddress; addr->mTtl = entry.mRecord.GetTtl(); } } aResult.mHostName = mName.AsCString(); aResult.mAddresses = aAddrArray.AsCArray(); aResult.mAddressesLength = aAddrArray.GetLength(); aResult.mInfraIfIndex = Get().mInfraIfIndex; } bool Core::AddrCache::ShouldStartInitialQueries(void) const { // This is called when the first active resolver is added // for this cache entry to determine whether we should // send initial queries. It is possible that we were passively // monitoring and has some cached records for this entry. // We send initial queries if there is no record or less than // half of original TTL remains on any record. bool shouldStart = false; TimeMilli now = TimerMilli::GetNow(); if (mCommittedEntries.IsEmpty()) { shouldStart = true; ExitNow(); } for (const AddrEntry &entry : mCommittedEntries) { if (entry.mRecord.LessThanHalfTtlRemains(now)) { shouldStart = true; ExitNow(); } } exit: return shouldStart; } void Core::AddrCache::AddNewResponseAddress(const Ip6::Address &aAddress, uint32_t aTtl, bool aCacheFlush) { // Adds a new address record to `mNewEntries` list. This called as // the records in a received response are processed one by one. // Once all records are processed `CommitNewResponseEntries()` is // called to update the list of addresses. AddrEntry *entry; if (aCacheFlush) { mShouldFlush = true; } // Check for duplicate addresses in the same response. entry = mNewEntries.FindMatching(aAddress); if (entry == nullptr) { entry = AddrEntry::Allocate(aAddress); OT_ASSERT(entry != nullptr); mNewEntries.Push(*entry); } entry->mRecord.RefreshTtl(aTtl); } void Core::AddrCache::CommitNewResponseEntries(void) { bool changed = false; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Determine whether there is any changes to the list of addresses // between the `mNewEntries` and `mCommittedEntries` lists. // // First, we verify if all new entries are present in the // `mCommittedEntries` list with the same TTL value. Next, if we // need to flush the old cache list, we check if any existing // `mCommittedEntries` is absent in `mNewEntries` list. for (const AddrEntry &newEntry : mNewEntries) { AddrEntry *exitingEntry = mCommittedEntries.FindMatching(newEntry.mAddress); if (newEntry.GetTtl() == 0) { // New entry has zero TTL, removing the address. If we // have a matching `exitingEntry` we set its TTL to zero // so to remove it in the next step when updating the // `mCommittedEntries` list. if (exitingEntry != nullptr) { exitingEntry->mRecord.RefreshTtl(0); changed = true; } } else if ((exitingEntry == nullptr) || (exitingEntry->GetTtl() != newEntry.GetTtl())) { changed = true; } } if (mShouldFlush && !changed) { for (const AddrEntry &exitingEntry : mCommittedEntries) { if ((exitingEntry.GetTtl() > 0) && !mNewEntries.ContainsMatching(exitingEntry.mAddress)) { changed = true; break; } } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Update the `mCommittedEntries` list. // First remove entries, if we need to flush clear everything, // otherwise remove the ones with zero TTL marked in previous // step. Then, add or update new entries to `mCommittedEntries` if (mShouldFlush) { mCommittedEntries.Clear(); mShouldFlush = false; } else { mCommittedEntries.RemoveAndFreeAllMatching(EmptyChecker()); } while (!mNewEntries.IsEmpty()) { OwnedPtr newEntry = mNewEntries.Pop(); AddrEntry *entry; if (newEntry->GetTtl() == 0) { continue; } entry = mCommittedEntries.FindMatching(newEntry->mAddress); if (entry != nullptr) { entry->mRecord.RefreshTtl(newEntry->GetTtl()); } else { mCommittedEntries.Push(*newEntry.Release()); } } StopInitialQueries(); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Invoke callbacks if there is any change. if (changed) { Heap::Array addrArray; AddressResult result; ConstructResult(result, addrArray); InvokeCallbacks(result); } DetermineNextFireTime(); ScheduleTimer(); } #if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE void Core::AddrCache::CopyInfoTo(AddressResolver &aResolver, CacheInfo &aInfo) const { aResolver.mHostName = mName.AsCString(); aResolver.mInfraIfIndex = Get().mInfraIfIndex; aResolver.mCallback = nullptr; aInfo.mIsActive = IsActive(); aInfo.mHasCachedResults = !mCommittedEntries.IsEmpty(); } #endif //--------------------------------------------------------------------------------------------------------------------- // Core::AddrCache::AddrEntry Core::AddrCache::AddrEntry::AddrEntry(const Ip6::Address &aAddress) : mNext(nullptr) , mAddress(aAddress) { } bool Core::AddrCache::AddrEntry::Matches(const ExpireChecker &aExpireChecker) const { return mRecord.ShouldExpire(aExpireChecker.mNow); } bool Core::AddrCache::AddrEntry::Matches(EmptyChecker aChecker) const { OT_UNUSED_VARIABLE(aChecker); return !mRecord.IsPresent(); } //--------------------------------------------------------------------------------------------------------------------- // Core::Ip6AddrCache Error Core::Ip6AddrCache::Init(Instance &aInstance, const char *aHostName) { return AddrCache::Init(aInstance, kIp6AddrCache, aHostName); } Error Core::Ip6AddrCache::Init(Instance &aInstance, const AddressResolver &aResolver) { return AddrCache::Init(aInstance, kIp6AddrCache, aResolver); } void Core::Ip6AddrCache::ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset) { // Name and record type in `aMessage` are already matched. AaaaRecord aaaaRecord; SuccessOrExit(aMessage.Read(aRecordOffset, aaaaRecord)); VerifyOrExit(aaaaRecord.GetLength() >= sizeof(Ip6::Address)); AddNewResponseAddress(aaaaRecord.GetAddress(), aaaaRecord.GetTtl(), aaaaRecord.GetClass() & kClassCacheFlushFlag); exit: return; } void Core::Ip6AddrCache::PrepareAaaaQuestion(TxMessage &aQuery) { PrepareQueryQuestion(aQuery, ResourceRecord::kTypeAaaa); } //--------------------------------------------------------------------------------------------------------------------- // Core::Ip4AddrCache Error Core::Ip4AddrCache::Init(Instance &aInstance, const char *aHostName) { return AddrCache::Init(aInstance, kIp4AddrCache, aHostName); } Error Core::Ip4AddrCache::Init(Instance &aInstance, const AddressResolver &aResolver) { return AddrCache::Init(aInstance, kIp4AddrCache, aResolver); } void Core::Ip4AddrCache::ProcessResponseRecord(const Message &aMessage, uint16_t aRecordOffset) { // Name and record type in `aMessage` are already matched. ARecord aRecord; Ip6::Address address; SuccessOrExit(aMessage.Read(aRecordOffset, aRecord)); VerifyOrExit(aRecord.GetLength() >= sizeof(Ip4::Address)); address.SetToIp4Mapped(aRecord.GetAddress()); AddNewResponseAddress(address, aRecord.GetTtl(), aRecord.GetClass() & kClassCacheFlushFlag); exit: return; } void Core::Ip4AddrCache::PrepareAQuestion(TxMessage &aQuery) { PrepareQueryQuestion(aQuery, ResourceRecord::kTypeA); } //--------------------------------------------------------------------------------------------------------------------- // Core::Iterator #if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE Core::EntryIterator::EntryIterator(Instance &aInstance) : InstanceLocator(aInstance) , mType(kUnspecified) { } Error Core::EntryIterator::GetNextHost(Host &aHost, EntryState &aState) { Error error = kErrorNotFound; if (mType == kUnspecified) { mHostEntry = Get().mHostEntries.GetHead(); mType = kHost; } else { VerifyOrExit(mType == kHost, error = kErrorInvalidArgs); } while (error == kErrorNotFound) { VerifyOrExit(mHostEntry != nullptr); error = mHostEntry->CopyInfoTo(aHost, aState); mHostEntry = mHostEntry->GetNext(); } exit: return error; } Error Core::EntryIterator::GetNextService(Service &aService, EntryState &aState) { Error error = kErrorNotFound; if (mType == kUnspecified) { mServiceEntry = Get().mServiceEntries.GetHead(); mType = kService; } else { VerifyOrExit(mType == kService, error = kErrorInvalidArgs); } while (error == kErrorNotFound) { VerifyOrExit(mServiceEntry != nullptr); error = mServiceEntry->CopyInfoTo(aService, aState, *this); mServiceEntry = mServiceEntry->GetNext(); } exit: return error; } Error Core::EntryIterator::GetNextKey(Key &aKey, EntryState &aState) { Error error = kErrorNotFound; if (mType == kUnspecified) { mHostEntry = Get().mHostEntries.GetHead(); mType = kHostKey; } else { VerifyOrExit((mType == kServiceKey) || (mType == kHostKey), error = kErrorInvalidArgs); } while ((error == kErrorNotFound) && (mType == kHostKey)) { if (mHostEntry == nullptr) { mServiceEntry = Get().mServiceEntries.GetHead(); mType = kServiceKey; break; } error = mHostEntry->CopyInfoTo(aKey, aState); mHostEntry = mHostEntry->GetNext(); } while ((error == kErrorNotFound) && (mType == kServiceKey)) { VerifyOrExit(mServiceEntry != nullptr); error = mServiceEntry->CopyInfoTo(aKey, aState); mServiceEntry = mServiceEntry->GetNext(); } exit: return error; } Error Core::EntryIterator::GetNextBrowser(Browser &aBrowser, CacheInfo &aInfo) { Error error = kErrorNone; if (mType == kUnspecified) { mBrowseCache = Get().mBrowseCacheList.GetHead(); mType = kBrowser; } else { VerifyOrExit(mType == kBrowser, error = kErrorInvalidArgs); } VerifyOrExit(mBrowseCache != nullptr, error = kErrorNotFound); mBrowseCache->CopyInfoTo(aBrowser, aInfo); mBrowseCache = mBrowseCache->GetNext(); exit: return error; } Error Core::EntryIterator::GetNextSrvResolver(SrvResolver &aResolver, CacheInfo &aInfo) { Error error = kErrorNone; if (mType == kUnspecified) { mSrvCache = Get().mSrvCacheList.GetHead(); mType = kSrvResolver; } else { VerifyOrExit(mType == kSrvResolver, error = kErrorInvalidArgs); } VerifyOrExit(mSrvCache != nullptr, error = kErrorNotFound); mSrvCache->CopyInfoTo(aResolver, aInfo); mSrvCache = mSrvCache->GetNext(); exit: return error; } Error Core::EntryIterator::GetNextTxtResolver(TxtResolver &aResolver, CacheInfo &aInfo) { Error error = kErrorNone; if (mType == kUnspecified) { mTxtCache = Get().mTxtCacheList.GetHead(); mType = kTxtResolver; } else { VerifyOrExit(mType == kTxtResolver, error = kErrorInvalidArgs); } VerifyOrExit(mTxtCache != nullptr, error = kErrorNotFound); mTxtCache->CopyInfoTo(aResolver, aInfo); mTxtCache = mTxtCache->GetNext(); exit: return error; } Error Core::EntryIterator::GetNextIp6AddressResolver(AddressResolver &aResolver, CacheInfo &aInfo) { Error error = kErrorNone; if (mType == kUnspecified) { mIp6AddrCache = Get().mIp6AddrCacheList.GetHead(); mType = kIp6AddrResolver; } else { VerifyOrExit(mType == kIp6AddrResolver, error = kErrorInvalidArgs); } VerifyOrExit(mIp6AddrCache != nullptr, error = kErrorNotFound); mIp6AddrCache->CopyInfoTo(aResolver, aInfo); mIp6AddrCache = mIp6AddrCache->GetNext(); exit: return error; } Error Core::EntryIterator::GetNextIp4AddressResolver(AddressResolver &aResolver, CacheInfo &aInfo) { Error error = kErrorNone; if (mType == kUnspecified) { mIp4AddrCache = Get().mIp4AddrCacheList.GetHead(); mType = kIp4AddrResolver; } else { VerifyOrExit(mType == kIp4AddrResolver, error = kErrorInvalidArgs); } VerifyOrExit(mIp4AddrCache != nullptr, error = kErrorNotFound); mIp4AddrCache->CopyInfoTo(aResolver, aInfo); mIp4AddrCache = mIp4AddrCache->GetNext(); exit: return error; } #endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE } // namespace Multicast } // namespace Dns } // namespace ot //--------------------------------------------------------------------------------------------------------------------- #if OPENTHREAD_CONFIG_MULTICAST_DNS_MOCK_PLAT_APIS_ENABLE OT_TOOL_WEAK otError otPlatMdnsSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex) { OT_UNUSED_VARIABLE(aInstance); OT_UNUSED_VARIABLE(aEnable); OT_UNUSED_VARIABLE(aInfraIfIndex); return OT_ERROR_FAILED; } OT_TOOL_WEAK void otPlatMdnsSendMulticast(otInstance *aInstance, otMessage *aMessage, uint32_t aInfraIfIndex) { OT_UNUSED_VARIABLE(aInstance); OT_UNUSED_VARIABLE(aMessage); OT_UNUSED_VARIABLE(aInfraIfIndex); } OT_TOOL_WEAK void otPlatMdnsSendUnicast(otInstance *aInstance, otMessage *aMessage, const otPlatMdnsAddressInfo *aAddress) { OT_UNUSED_VARIABLE(aInstance); OT_UNUSED_VARIABLE(aMessage); OT_UNUSED_VARIABLE(aAddress); } #endif // OPENTHREAD_CONFIG_MULTICAST_DNS_MOCK_PLAT_APIS_ENABLE #endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE