Files
openthread/src/core/net/mdns.cpp
T
Abtin Keshavarzian 4de7bc578e [random] introduce template-based NonCrypto random APIs (#13142)
This commit introduces a new set of template-based APIs for
non-cryptographic random number generation in the `Random::NonCrypto`
namespace. These new methods provide a cleaner, type-safe, and more
robust interface compared to the previous methods.

Key additions:
- `Generate<UintType>()`: Returns a random value of the given
  unsigned integer type (`uint8_t`, `uint16_t`, or `uint32_t`).
- `GenerateUpToExcluding<UintType>(aMax)`: Returns a random value in
  the range `[0, aMax)`.
- `GenerateFromMinUpToExcluding<UintType>(aMin, aMax)`: Returns a
  random value in the range `[aMin, aMax)`.
- `GenerateInClosedRange<UintType>(aMin, aMax)`: Returns a random
  value in the closed range `[aMin, aMax]`.

The introduction of `GenerateInClosedRange` is an improvement as it
safely handles ranges up to the maximum value of the integer type
(e.g., `0xffff`) without the risk of overflow.

All call sites across the OpenThread core stack and tests have been
updated to adopt these new APIs. The public `otRandomNonCrypto`
functions are also updated to leverage the new internal methods.

Doxygen documentation is added for all new template methods,
detailing their behavior, including edge cases where the upper bound
is smaller than or equal to the lower bound.
2026-05-25 19:39:59 -07:00

8209 lines
223 KiB
C++

/*
* 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");
#if OPENTHREAD_CONFIG_MULTICAST_DNS_VERBOSE_LOGGING_ENABLE
#define LogVerbose(...) \
if (Get<Core>().mVerboseLogging) \
LogAt(kLogLevelNone, __VA_ARGS__)
#else
#define LogVerbose(...)
#endif
//---------------------------------------------------------------------------------------------------------------------
// otPlatMdns callbacks
extern "C" void otPlatMdnsHandleReceive(otInstance *aInstance,
otMessage *aMessage,
bool aIsUnicast,
const otPlatMdnsAddressInfo *aAddress)
{
AsCoreType(aInstance).Get<Core>().HandleMessage(AsCoreType(aMessage), aIsUnicast, AsCoreType(aAddress));
}
extern "C" void otPlatMdnsHandleHostAddressEvent(otInstance *aInstance,
const otIp6Address *aAddress,
bool aAdded,
uint32_t aInfraIfIndex)
{
AsCoreType(aInstance).Get<Core>().HandleHostAddressEvent(AsCoreType(aAddress), aAdded, aInfraIfIndex);
}
extern "C" void otPlatMdnsHandleHostAddressRemoveAll(otInstance *aInstance, uint32_t aInfraIfIndex)
{
AsCoreType(aInstance).Get<Core>().HandleHostAddressRemoveAll(aInfraIfIndex);
}
//----------------------------------------------------------------------------------------------------------------------
// 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)
, mAutoEnable(kDefaultAutoEnable)
, mIsQuestionUnicastAllowed(kDefaultQuAllowed)
, mMaxMessageSize(kMaxMessageSize)
, mInfraIfIndex(0)
, mLocalHost(aInstance)
, mMultiPacketRxMessages(aInstance)
, mNextProbeTxTime(TimerMilli::GetNow() - 1)
, mEntryTimer(aInstance)
, mEntryTask(aInstance)
, mTxMessageHistory(aInstance)
, mConflictCallback(nullptr)
, mNextQueryTxTime(TimerMilli::GetNow() - 1)
, mCacheTimer(aInstance)
, mCacheTask(aInstance)
#if OPENTHREAD_CONFIG_MULTICAST_DNS_VERBOSE_LOGGING_ENABLE
, mVerboseLogging(kDefaultVerboseLog)
#endif
{
}
void Core::AfterInstanceInit(void)
{
// This is called immediately after the OpenThread `Instance` is
// initialized (i.e., after all constructors are called and saved
// information from `Settings` is restored). This call triggers
// the generation of the local host name, which is derived from
// the device's extended MAC address. This ensures that the MAC
// address is restored from the non-volatile settings, and the
// generated name remains consistent across device reboots.
mLocalHost.GenerateName();
}
Error Core::SetEnabled(bool aEnable, uint32_t aInfraIfIndex, Requester aRequester)
{
Error error = kErrorNone;
if (aRequester == kRequesterUser)
{
mAutoEnable = false;
}
VerifyOrExit(aEnable != mIsEnabled);
mIsEnabled = aEnable;
mInfraIfIndex = aInfraIfIndex;
SuccessOrExit(error = otPlatMdnsSetListeningEnabled(&GetInstance(), aEnable, aInfraIfIndex));
if (mIsEnabled)
{
LogInfo("%snabling on infra-if-index %lu", (aRequester == kRequesterAuto) ? "Auto-e" : "E",
ToUlong(mInfraIfIndex));
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
SuccessOrAssert(GetInstance().SetDnsNameCompressionEnabled(true));
#endif
}
else
{
LogInfo("%sisabling", (aRequester == kRequesterAuto) ? "Auto-d" : "D");
mLocalHost.ClearAddresses();
mHostEntries.Clear();
mServiceEntries.Clear();
mServiceTypes.Clear();
mMultiPacketRxMessages.Clear();
mTxMessageHistory.Clear();
mEntryTimer.Stop();
mBrowseCacheList.Clear();
mSrvCacheList.Clear();
mTxtCacheList.Clear();
mIp6AddrCacheList.Clear();
mIp4AddrCacheList.Clear();
mRecordCacheList.Clear();
mCacheTimer.Stop();
}
Get<Dnssd>().HandleMdnsCoreStateChange();
exit:
return error;
}
#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
void Core::SetAutoEnableMode(bool aEnable)
{
VerifyOrExit(mAutoEnable != aEnable);
mAutoEnable = aEnable;
if (mAutoEnable)
{
if (!Get<BorderRouter::InfraIf>().IsRunning())
{
IgnoreError(SetEnabled(false, mInfraIfIndex, kRequesterAuto));
ExitNow();
}
if (IsEnabled())
{
VerifyOrExit(Get<BorderRouter::InfraIf>().GetIfIndex() != mInfraIfIndex);
IgnoreError(SetEnabled(false, mInfraIfIndex, kRequesterAuto));
}
IgnoreError(SetEnabled(true, Get<BorderRouter::InfraIf>().GetIfIndex(), kRequesterAuto));
}
exit:
return;
}
void Core::HandleInfraIfStateChanged(void)
{
VerifyOrExit(mAutoEnable);
IgnoreError(SetEnabled(Get<BorderRouter::InfraIf>().IsRunning(), Get<BorderRouter::InfraIf>().GetIfIndex(),
kRequesterAuto));
exit:
return;
}
#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
Error Core::ValidateHostName(const Host &aHost) const { return Name::ValidateName(aHost.mHostName); }
Error Core::ValidateServiceNames(const Service &aService, bool aCheckHostAndSubTypeLabels) const
{
Error error;
SuccessOrExit(error = Name::ValidateLabel(aService.mServiceInstance));
SuccessOrExit(error = Name::ValidateName(aService.mServiceType));
if (aCheckHostAndSubTypeLabels && aService.mHostName != nullptr)
{
SuccessOrExit(error = Name::ValidateName(aService.mHostName));
}
if (aCheckHostAndSubTypeLabels && (aService.mSubTypeLabelsLength > 0))
{
VerifyOrExit(aService.mSubTypeLabels != nullptr, error = kErrorInvalidArgs);
for (uint16_t index = 0; index < aService.mSubTypeLabelsLength; index++)
{
SuccessOrExit(error = Name::ValidateLabel(aService.mSubTypeLabels[index]));
}
}
exit:
return error;
}
Error Core::ValidateKeyName(const Key &aKey) const
{
Error error;
if (IsKeyForService(aKey))
{
SuccessOrExit(error = Name::ValidateName(aKey.mServiceType));
error = Name::ValidateLabel(aKey.mName);
}
else
{
error = Name::ValidateName(aKey.mName);
}
exit:
return error;
}
template <typename EntryType, typename ItemInfo>
Error Core::Register(const ItemInfo &aItemInfo, RequestId aRequestId, RegisterCallback aCallback)
{
Error error = kErrorNone;
EntryType *entry;
VerifyOrExit(mIsEnabled, error = kErrorInvalidState);
entry = GetEntryList<EntryType>().FindMatching(aItemInfo);
if (entry == nullptr)
{
entry = EntryType::AllocateAndInit(GetInstance(), aItemInfo);
OT_ASSERT(entry != nullptr);
GetEntryList<EntryType>().Push(*entry);
}
entry->Register(aItemInfo, Callback(aRequestId, aCallback));
exit:
return error;
}
template <typename EntryType, typename ItemInfo> Error Core::Unregister(const ItemInfo &aItemInfo)
{
Error error = kErrorNone;
EntryType *entry;
VerifyOrExit(mIsEnabled, error = kErrorInvalidState);
entry = GetEntryList<EntryType>().FindMatching(aItemInfo);
if (entry != nullptr)
{
entry->Unregister(aItemInfo);
}
exit:
return error;
}
Error Core::RegisterHost(const Host &aHost, RequestId aRequestId, RegisterCallback aCallback)
{
Error error;
SuccessOrExit(error = ValidateHostName(aHost));
error = Register<HostEntry>(aHost, aRequestId, aCallback);
exit:
return error;
}
Error Core::UnregisterHost(const Host &aHost)
{
Error error;
SuccessOrExit(error = ValidateHostName(aHost));
error = Unregister<HostEntry>(aHost);
exit:
return error;
}
Error Core::RegisterService(const Service &aService, RequestId aRequestId, RegisterCallback aCallback)
{
Error error;
SuccessOrExit(error = ValidateServiceNames(aService, /* aCheckHostAndSubTypeLabels */ true));
error = Register<ServiceEntry>(aService, aRequestId, aCallback);
exit:
return error;
}
Error Core::UnregisterService(const Service &aService)
{
Error error;
SuccessOrExit(error = ValidateServiceNames(aService, /* aCheckHostAndSubTypeLabels */ false));
error = Unregister<ServiceEntry>(aService);
exit:
return error;
}
Error Core::RegisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback)
{
Error error;
SuccessOrExit(error = ValidateKeyName(aKey));
error = IsKeyForService(aKey) ? Register<ServiceEntry>(aKey, aRequestId, aCallback)
: Register<HostEntry>(aKey, aRequestId, aCallback);
exit:
return error;
}
Error Core::UnregisterKey(const Key &aKey)
{
Error error;
SuccessOrExit(error = ValidateKeyName(aKey));
error = IsKeyForService(aKey) ? Unregister<ServiceEntry>(aKey) : Unregister<HostEntry>(aKey);
exit:
return error;
}
#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<EntryIterator &>(aIterator).Free(); }
Error Core::GetNextHost(Iterator &aIterator, Host &aHost, EntryState &aState) const
{
return static_cast<EntryIterator &>(aIterator).GetNextHost(aHost, aState);
}
Error Core::GetNextService(Iterator &aIterator, Service &aService, EntryState &aState) const
{
return static_cast<EntryIterator &>(aIterator).GetNextService(aService, aState);
}
Error Core::GetNextKey(Iterator &aIterator, Key &aKey, EntryState &aState) const
{
return static_cast<EntryIterator &>(aIterator).GetNextKey(aKey, aState);
}
Error Core::GetNextLocalHostAddress(Iterator &aIterator, LocalHostAddress &aAddress)
{
return static_cast<EntryIterator &>(aIterator).GetNextLocalHostAddress(aAddress);
}
Error Core::GetNextBrowser(Iterator &aIterator, Browser &aBrowser, CacheInfo &aInfo) const
{
return static_cast<EntryIterator &>(aIterator).GetNextBrowser(aBrowser, aInfo);
}
Error Core::GetNextSrvResolver(Iterator &aIterator, SrvResolver &aResolver, CacheInfo &aInfo) const
{
return static_cast<EntryIterator &>(aIterator).GetNextSrvResolver(aResolver, aInfo);
}
Error Core::GetNextTxtResolver(Iterator &aIterator, TxtResolver &aResolver, CacheInfo &aInfo) const
{
return static_cast<EntryIterator &>(aIterator).GetNextTxtResolver(aResolver, aInfo);
}
Error Core::GetNextIp6AddressResolver(Iterator &aIterator, AddressResolver &aResolver, CacheInfo &aInfo) const
{
return static_cast<EntryIterator &>(aIterator).GetNextIp6AddressResolver(aResolver, aInfo);
}
Error Core::GetNextIp4AddressResolver(Iterator &aIterator, AddressResolver &aResolver, CacheInfo &aInfo) const
{
return static_cast<EntryIterator &>(aIterator).GetNextIp4AddressResolver(aResolver, aInfo);
}
Error Core::GetNextRecordQuerier(Iterator &aIterator, RecordQuerier &aQuerier, CacheInfo &aInfo) const
{
return static_cast<EntryIterator &>(aIterator).GetNextRecordQuerier(aQuerier, 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::HandleHostAddressEvent(const Ip6::Address &aAddress, bool aAdded, uint32_t aInfraIfIndex)
{
mLocalHost.HandleAddressEvent(aAddress, aAdded, aInfraIfIndex);
}
void Core::HandleHostAddressRemoveAll(uint32_t aInfraIfIndex) { mLocalHost.HandleAddressRemoveAll(aInfraIfIndex); }
void Core::HandleMessage(Message &aMessage, bool aIsUnicast, const AddressInfo &aSenderAddress)
{
OwnedPtr<Message> messagePtr(&aMessage);
OwnedPtr<RxMessage> 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::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);
}
#if OPENTHREAD_CONFIG_MULTICAST_DNS_VERBOSE_LOGGING_ENABLE
void Core::SetVerboseLoggingEnabled(bool aEnable)
{
VerifyOrExit(mVerboseLogging != aEnable);
if (aEnable)
{
mVerboseLogging = true;
LogVerbose("Verbose logging enabled");
}
else
{
LogVerbose("Verbose logging disabled");
mVerboseLogging = false;
}
exit:
return;
}
void Core::LogMessage(const Message &aMessage)
{
if (mVerboseLogging)
{
MsgLogger logger(GetInstance(), aMessage);
logger.Log();
}
}
#endif
//----------------------------------------------------------------------------------------------------------------------
// Core::AddressInfo
Core::AddressInfo::InfoString Core::AddressInfo::ToString(void) const
{
InfoString string;
string.Append("[%s]:%u if-index:%lu", GetAddress().ToString().AsCString(), mPort, ToUlong(mInfraIfIndex));
return string;
}
//----------------------------------------------------------------------------------------------------------------------
// 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;
}
bool Core::AddressArray::Matches(const AddressArray &aOther) const
{
return Matches(aOther.AsCArray(), aOther.GetLength());
}
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 <typename UintType> 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::IsUint<UintType>::kValue, "UintType must be an unsigned int (8, 16, 32, or 64 bit len)");
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;
SetToUintMax(duration);
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)
{
DecideToProbeOnRegister();
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<Core>().mEntryTask.Post();
}
void Core::Entry::ScheduleCallbackTask(void)
{
switch (GetState())
{
case kRegistered:
case kConflict:
VerifyOrExit(!mCallback.IsEmpty() || !mKeyCallback.IsEmpty());
Get<Core>().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::DecideToProbeOnRegister(void)
{
// Checks whether we should start probing when `Register()` is
// called. If a conflict was previously detected, we send a probe
// again upon an explicit `Register()` request.
switch (mState)
{
case kRegistered:
case kProbing:
break;
case kRemoving:
case kConflict:
StartProbing();
break;
}
}
void Core::Entry::StartProbing(void)
{
SetState(kProbing);
mProbeCount = 0;
SetFireTime(Get<Core>().RandomizeFirstProbeTxTime());
ScheduleTimer();
}
void Core::Entry::SetStateToConflict(void)
{
switch (GetState())
{
case kProbing:
SetState(kConflict);
break;
case kRegistered:
#if !OPENTHREAD_CONFIG_MULTICAST_DNS_PERSIST_STATE_ON_POST_PROBE_CONFLICT
SetState(kConflict);
#endif
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, RecordAndTypeArray &aRecordAndTypes)
{
// Schedule answers for all matching records in `aRecords` array
// to a given non-probe question.
bool allEmptyOrZeroTtl = true;
bool answerNsec = true;
for (RecordAndType &recordAndType : aRecordAndTypes)
{
RecordInfo &record = *recordAndType.mRecord;
if (!record.CanAnswer())
{
// Cannot answer if record is not present or has zero TTL.
continue;
}
allEmptyOrZeroTtl = false;
if (QuestionMatches(aInfo.mQuestionRrType, recordAndType.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, RecordAndTypeArray &aRecordAndTypes)
{
bool allEmptyOrZeroTtl = true;
bool shouldDelay = false;
TimeMilli now = TimerMilli::GetNow();
AnswerInfo info = aInfo;
info.mAnswerDelay = 0;
OT_ASSERT(info.mIsProbe);
for (RecordAndType &recordAndType : aRecordAndTypes)
{
RecordInfo &record = *recordAndType.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 (RecordAndType &recordAndType : aRecordAndTypes)
{
recordAndType.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<Core>().mEntryTimer); }
template <typename EntryType> void Core::Entry::HandleTimer(EntryContext &aContext)
{
EntryType *thisAsEntryType = static_cast<EntryType *>(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<Core>().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()));
ResourceRecord::UpdateRecordLengthInMessage(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<Core>().mInfraIfIndex;
aState = static_cast<EntryState>(GetState());
exit:
return error;
}
//----------------------------------------------------------------------------------------------------------------------
// Core::Entry::RecordAndTypeArray
void Core::Entry::RecordAndTypeArray::Add(RecordInfo &aRecord, uint16_t aType)
{
RecordAndType *entry = PushBack();
OT_ASSERT(entry != nullptr);
entry->mRecord = &aRecord;
entry->mType = aType;
}
//----------------------------------------------------------------------------------------------------------------------
// Core::LocalHost
Core::LocalHost::LocalHost(Instance &aInstance)
: InstanceLocator(aInstance)
, mEventTimer(aInstance)
{
GenerateName();
}
Error Core::LocalHost::SetName(const char *aName)
{
Error error = kErrorNone;
VerifyOrExit(!Get<Core>().mIsEnabled, error = kErrorInvalidState);
if (aName == nullptr)
{
GenerateName();
}
else
{
SuccessOrAssert(mName.Set(aName));
}
exit:
return error;
}
void Core::LocalHost::GenerateName(void)
{
Name::LabelBuffer name;
StringWriter writer(name, sizeof(name));
writer.Append("ot%s", Get<Mac::Mac>().GetExtAddress().ToString().AsCString());
SuccessOrAssert(mName.Set(name));
}
void Core::LocalHost::ClearAddresses(void)
{
mIp4Addresses.Free();
mIp6Addresses.Free();
mAddrEvents.Clear();
mEventTimer.Stop();
}
void Core::LocalHost::HandleAddressEvent(const Ip6::Address &aAddress, bool aAdded, uint32_t aInfraIfIndex)
{
AddrEvent *addrEvent;
VerifyOrExit(Get<Core>().mIsEnabled);
VerifyOrExit(aInfraIfIndex == Get<Core>().mInfraIfIndex);
LogDebg("Host address %s event: %s", aAddress.ToString().AsCString(), aAdded ? "add" : "remove");
addrEvent = AddrEvent::Allocate(aAddress, aAdded);
OT_ASSERT(addrEvent != nullptr);
// Before we add the new event, we remove any previous events in the
// list that match the same address. This way we always track the
// latest event for each address. This handles the case where
// a "removed" address event is quickly followed by an "added" event
// for the same address.
//
// The events are processed after a short guard delay time
// `kGuardTimeToProcessAddrEvents`. This ensures multiple changes
// to be grouped and announced together.
mAddrEvents.RemoveAndFreeAllMatching(aAddress);
mAddrEvents.Push(*addrEvent);
if (!mEventTimer.IsRunning())
{
mEventTimer.Start(kGuardTimeToProcessAddrEvents);
}
exit:
return;
}
void Core::LocalHost::HandleAddressRemoveAll(uint32_t aInfraIfIndex)
{
VerifyOrExit(Get<Core>().mIsEnabled);
VerifyOrExit(aInfraIfIndex == Get<Core>().mInfraIfIndex);
mAddrEvents.Clear();
mEventTimer.Stop();
LogDebg("Host address event: remove all");
for (const Ip6::Address &address : mIp4Addresses)
{
HandleAddressEvent(address, /* aAdded */ false, aInfraIfIndex);
}
for (const Ip6::Address &address : mIp6Addresses)
{
HandleAddressEvent(address, /* aAdded */ false, aInfraIfIndex);
}
exit:
return;
}
void Core::LocalHost::HandleEventTimer(void)
{
// Process all saved `AddrEvents` and update IPv4 and IPv6
// address lists.
static const AddrType kAddrTypes[] = {kIp4AddrType, kIp6AddrType};
VerifyOrExit(Get<Core>().mIsEnabled);
for (AddrType addrType : kAddrTypes)
{
AddressArray &addresses = (addrType == kIp4AddrType) ? mIp4Addresses : mIp6Addresses;
AddressArray oldAddresses;
oldAddresses.TakeFrom(addresses.Move());
addresses.Clear();
// First, add existing addresses (from old list) that did not
// change (there is no "removed" event).
for (const Ip6::Address &address : oldAddresses)
{
const AddrEvent *addrEvent = mAddrEvents.FindMatching(address);
if ((addrEvent == nullptr) || addrEvent->mAdded)
{
SuccessOrAssert(addresses.PushBack(address));
}
else
{
LogAddressChange(/* aAdded */ false, addrType, address);
}
}
// Next, add any new addresses for which we got an "added"
// event.
for (const AddrEvent &addrEvent : mAddrEvents)
{
if (!addrEvent.Matches(addrType))
{
continue;
}
if (addrEvent.mAdded && !addresses.Contains(addrEvent.mAddress))
{
SuccessOrAssert(addresses.PushBack(addrEvent.mAddress));
LogAddressChange(/* aAdded */ true, addrType, addrEvent.mAddress);
}
}
}
IgnoreError(Get<Core>().Register<HostEntry>(*this, /* aRequestId */ 0, /* aCallback */ nullptr));
exit:
mAddrEvents.Clear();
}
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
void Core::LocalHost::LogAddressChange(bool aAdded, AddrType aAddrType, const Ip6::Address &aAddress) const
{
Ip4::Address ip4Address;
if (aAddrType == kIp4AddrType)
{
SuccessOrAssert(ip4Address.ExtractFromIp4MappedIp6Address(aAddress));
}
LogInfo("%s host address %s", aAdded ? "Adding" : "Removing",
aAddrType == kIp4AddrType ? ip4Address.ToString().AsCString() : aAddress.ToString().AsCString());
}
#else
void Core::LocalHost::LogAddressChange(bool, AddrType, const Ip6::Address &) const {}
#endif
//----------------------------------------------------------------------------------------------------------------------
// Core::LocalHost::AddrEvent
Core::LocalHost::AddrEvent::AddrEvent(const Ip6::Address &aAddress, bool aAdded)
: mNext(nullptr)
, mAddress(aAddress)
, mAdded(aAdded)
{
}
bool Core::LocalHost::AddrEvent::Matches(AddrType aType) const
{
bool matches = false;
bool isIp4 = mAddress.IsIp4Mapped();
switch (aType)
{
case kIp4AddrType:
matches = isIp4;
break;
case kIp6AddrType:
matches = !isIp4;
break;
}
return matches;
}
//----------------------------------------------------------------------------------------------------------------------
// 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 LocalHost &aLocalHost) const { return NameMatch(mName, aLocalHost.GetName()); }
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
{
bool isEmpty = false;
VerifyOrExit(!mKeyRecord.IsPresent() && !mIp6AddrRecord.IsPresent());
VerifyOrExit((mIp4AddrRecord == nullptr) || !mIp4AddrRecord->IsPresent());
isEmpty = true;
exit:
return isEmpty;
}
void Core::HostEntry::Register(const Host &aHost, const Callback &aCallback)
{
DecideToProbeOnRegister();
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();
}
mIp6AddrRecord.UpdateTtl(DetermineTtl(aHost.mTtl, kDefaultAddrTtl));
mIp6AddrRecord.UpdateAddresses(aHost);
DetermineNextFireTime();
ScheduleTimer();
exit:
return;
}
void Core::HostEntry::Register(const LocalHost &aLocalHost, const Callback &aCallback)
{
SetCallback(aCallback);
if (aLocalHost.GetIp6Addresses().IsEmpty())
{
if (mIp6AddrRecord.IsPresent())
{
mIp6AddrRecord.UpdateTtl(0);
}
}
else
{
mIp6AddrRecord.UpdateTtl(kDefaultAddrTtl);
mIp6AddrRecord.UpdateAddresses(aLocalHost.GetIp6Addresses());
}
if (aLocalHost.GetIp4Addresses().IsEmpty())
{
if ((mIp4AddrRecord != nullptr) && mIp4AddrRecord->IsPresent())
{
mIp4AddrRecord->UpdateTtl(0);
}
}
else
{
if (mIp4AddrRecord == nullptr)
{
mIp4AddrRecord.Reset(AddrRecord::Allocate());
OT_ASSERT(mIp4AddrRecord != nullptr);
}
mIp4AddrRecord->UpdateTtl(kDefaultAddrTtl);
mIp4AddrRecord->UpdateAddresses(aLocalHost.GetIp4Addresses());
}
DetermineNextFireTime();
ScheduleTimer();
}
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(mIp6AddrRecord.IsPresent());
ClearCallback();
switch (GetState())
{
case kRegistered:
mIp6AddrRecord.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)
{
mIp6AddrRecord.Clear();
if (mIp4AddrRecord != nullptr)
{
mIp4AddrRecord->Clear();
}
}
void Core::HostEntry::ScheduleToRemoveIfEmpty(void)
{
if (IsEmpty())
{
SetStateToRemoving();
Get<Core>().mEntryTask.Post();
}
}
void Core::HostEntry::HandleConflict(void)
{
State oldState = GetState();
SetStateToConflict();
VerifyOrExit(oldState == kRegistered);
Get<Core>().InvokeConflictCallback(mName.AsCString(), nullptr);
exit:
return;
}
void Core::HostEntry::AnswerQuestion(const AnswerInfo &aInfo)
{
RecordAndTypeArray recordAndTypes;
VerifyOrExit(GetState() == kRegistered);
recordAndTypes.Add(mIp6AddrRecord, ResourceRecord::kTypeAaaa);
recordAndTypes.Add(mKeyRecord, ResourceRecord::kTypeKey);
if (mIp4AddrRecord != nullptr)
{
recordAndTypes.Add(*mIp4AddrRecord, ResourceRecord::kTypeA);
}
if (aInfo.mIsProbe)
{
AnswerProbe(aInfo, recordAndTypes);
}
else
{
AnswerNonProbe(aInfo, recordAndTypes);
}
DetermineNextFireTime();
ScheduleTimer();
exit:
return;
}
void Core::HostEntry::HandleTimer(EntryContext &aContext) { Entry::HandleTimer<HostEntry>(aContext); }
void Core::HostEntry::ClearAppendState(void)
{
// Clears `HostEntry` records and all tracked saved name
// compression offsets.
Entry::ClearAppendState();
mIp6AddrRecord.MarkAsNotAppended();
if (mIp4AddrRecord != nullptr)
{
mIp4AddrRecord->MarkAsNotAppended();
}
mNameOffset = kUnspecifiedOffset;
}
void Core::HostEntry::PrepareProbe(TxMessage &aProbe)
{
bool prepareAgain = false;
do
{
aProbe.SaveCurrentState();
AppendNameTo(aProbe, kQuestionSection);
AppendQuestionTo(aProbe);
AppendIp6AddressRecordsTo(aProbe, kAuthoritySection);
AppendIp4AddressRecordsTo(aProbe, kAuthoritySection);
AppendKeyRecordTo(aProbe, kAuthoritySection);
aProbe.CheckSizeLimitToPrepareAgain(prepareAgain);
} while (prepareAgain);
}
void Core::HostEntry::StartAnnouncing(void)
{
mIp6AddrRecord.StartAnnouncing();
if (mIp4AddrRecord != nullptr)
{
mIp4AddrRecord->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 (mIp6AddrRecord.ShouldAppendTo(aContext))
{
AppendIp6AddressRecordsTo(response, kAnswerSection);
appendNsec = true;
}
if ((mIp4AddrRecord != nullptr) && mIp4AddrRecord->ShouldAppendTo(aContext))
{
AppendIp4AddressRecordsTo(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);
mIp6AddrRecord.UpdateStateAfterAnswer(aResponse);
if (mIp4AddrRecord != nullptr)
{
mIp4AddrRecord->UpdateStateAfterAnswer(aResponse);
}
if (IsEmpty())
{
SetStateToRemoving();
}
}
void Core::HostEntry::DetermineNextFireTime(void)
{
VerifyOrExit(GetState() == kRegistered);
Entry::DetermineNextFireTime();
mIp6AddrRecord.UpdateFireTimeOn(*this);
if (mIp4AddrRecord != nullptr)
{
mIp4AddrRecord->UpdateFireTimeOn(*this);
}
exit:
return;
}
void Core::HostEntry::DetermineNextAggrTxTime(NextFireTime &aNextAggrTxTime) const
{
VerifyOrExit(GetState() == kRegistered);
Entry::DetermineNextAggrTxTime(aNextAggrTxTime);
mIp6AddrRecord.DetermineNextAggrTxTime(aNextAggrTxTime);
if (mIp4AddrRecord != nullptr)
{
mIp4AddrRecord->DetermineNextAggrTxTime(aNextAggrTxTime);
}
exit:
return;
}
void Core::HostEntry::AppendIp6AddressRecordsTo(TxMessage &aTxMessage, Section aSection)
{
AppendAddressRecordsTo(aTxMessage, aSection, mIp6AddrRecord, /* aIp6 */ true);
}
void Core::HostEntry::AppendIp4AddressRecordsTo(TxMessage &aTxMessage, Section aSection)
{
if (mIp4AddrRecord != nullptr)
{
AppendAddressRecordsTo(aTxMessage, aSection, *mIp4AddrRecord, /* aIp6 */ false);
}
}
void Core::HostEntry::AppendAddressRecordsTo(TxMessage &aTxMessage,
Section aSection,
AddrRecord &aAddrRecord,
bool aIp6)
{
Message *message;
bool isLegacyUnicast = (aTxMessage.GetType() == TxMessage::kLegacyUnicastResponse);
VerifyOrExit(aAddrRecord.CanAppend());
aAddrRecord.MarkAsAppended(aTxMessage, aSection);
message = &aTxMessage.SelectMessageFor(aSection);
for (const Ip6::Address &address : aAddrRecord.mAddresses)
{
AppendNameTo(aTxMessage, aSection);
if (aIp6)
{
AaaaRecord aaaaRecord;
aaaaRecord.Init();
aaaaRecord.SetAddress(address);
aaaaRecord.SetTtl(aAddrRecord.GetTtl(isLegacyUnicast));
UpdateCacheFlushFlagIn(aaaaRecord, aSection, isLegacyUnicast);
SuccessOrAssert(message->Append(aaaaRecord));
}
else
{
Ip4::Address ip4Address;
ARecord aRecord;
SuccessOrAssert(ip4Address.ExtractFromIp4MappedIp6Address(address));
aRecord.Init();
aRecord.SetAddress(ip4Address);
aRecord.SetTtl(aAddrRecord.GetTtl(isLegacyUnicast));
UpdateCacheFlushFlagIn(aRecord, aSection, isLegacyUnicast);
SuccessOrAssert(message->Append(aRecord));
}
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 (mIp6AddrRecord.IsPresent() && (mIp6AddrRecord.GetTtl() > 0))
{
types.Add(ResourceRecord::kTypeAaaa);
}
if ((mIp4AddrRecord != nullptr) && mIp4AddrRecord->IsPresent() && (mIp4AddrRecord->GetTtl() > 0))
{
types.Add(ResourceRecord::kTypeA);
}
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<HostEntry &>(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;
}
void Core::HostEntry::MarkToAppendAddrRecordsInAdditionalData(void)
{
mIp6AddrRecord.MarkToAppendInAdditionalData();
if (mIp4AddrRecord != nullptr)
{
mIp4AddrRecord->MarkToAppendInAdditionalData();
}
}
#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE
Error Core::HostEntry::CopyInfoTo(Host &aHost, EntryState &aState) const
{
Error error = kErrorNone;
VerifyOrExit(mIp6AddrRecord.IsPresent(), error = kErrorNotFound);
aHost.mHostName = mName.AsCString();
aHost.mAddresses = mIp6AddrRecord.mAddresses.AsCArray();
aHost.mAddressesLength = mIp6AddrRecord.mAddresses.GetLength();
aHost.mTtl = mIp6AddrRecord.GetTtl();
aHost.mInfraIfIndex = Get<Core>().mInfraIfIndex;
aState = static_cast<EntryState>(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::HostEntry::AddrRecord
void Core::HostEntry::AddrRecord::Clear(void)
{
RecordInfo::Clear();
mAddresses.Free();
}
void Core::HostEntry::AddrRecord::UpdateAddresses(const Host &aHost)
{
UpdateProperty(mAddresses, AsCoreTypePtr(aHost.mAddresses), aHost.mAddressesLength);
}
void Core::HostEntry::AddrRecord::UpdateAddresses(const AddressArray &aAddresses)
{
UpdateProperty(mAddresses, aAddresses.AsCArray(), aAddresses.GetLength());
}
//----------------------------------------------------------------------------------------------------------------------
// 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)
{
const char *hostName;
uint32_t ttl = DetermineTtl(aService.mTtl, kDefaultServiceTtl);
DecideToProbeOnRegister();
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.
hostName = (aService.mHostName != nullptr) ? aService.mHostName : Get<Core>().mLocalHost.GetName();
mSrvRecord.UpdateTtl(ttl);
mSrvRecord.UpdateProperty(mHostName, hostName);
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<Core>().mEntryTask.Post();
}
}
void Core::ServiceEntry::HandleConflict(void)
{
State oldState = GetState();
SetStateToConflict();
UpdateServiceTypes();
VerifyOrExit(oldState == kRegistered);
Get<Core>().InvokeConflictCallback(mServiceInstance.AsCString(), mServiceType.AsCString());
exit:
return;
}
void Core::ServiceEntry::AnswerServiceNameQuestion(const AnswerInfo &aInfo)
{
RecordAndTypeArray recordAndTypes;
VerifyOrExit(GetState() == kRegistered);
recordAndTypes.Add(mSrvRecord, ResourceRecord::kTypeSrv);
recordAndTypes.Add(mTxtRecord, ResourceRecord::kTypeTxt);
recordAndTypes.Add(mKeyRecord, ResourceRecord::kTypeKey);
if (aInfo.mIsProbe)
{
AnswerProbe(aInfo, recordAndTypes);
}
else
{
AnswerNonProbe(aInfo, recordAndTypes);
}
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<ServiceEntry>(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->MarkToAppendAddrRecordsInAdditionalData();
}
}
if (mSrvRecord.ShouldAppendTo(aContext))
{
AppendSrvRecordTo(response, kAnswerSection);
appendNsec = true;
if ((mSrvRecord.GetTtl() > 0) && (hostEntry != nullptr))
{
hostEntry->MarkToAppendAddrRecordsInAdditionalData();
}
}
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)
{
if (hostEntry->mIp6AddrRecord.ShouldAppendInAdditionalDataSection())
{
hostEntry->AppendIp6AddressRecordsTo(response, kAdditionalDataSection);
}
if ((hostEntry->mIp4AddrRecord != nullptr) && hostEntry->mIp4AddrRecord->ShouldAppendInAdditionalDataSection())
{
hostEntry->AppendIp4AddressRecordsTo(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.
// TODO: Need to handle name matching host name
aHostEntry = Get<Core>().mHostEntries.FindMatching(mHostName);
if ((aHostEntry != nullptr) && (aHostEntry->GetState() != GetState()))
{
aHostEntry = nullptr;
}
if (aHostEntry != nullptr)
{
UpdateCompressOffset(mHostNameOffset, aHostEntry->mNameOffset);
}
for (ServiceEntry &other : Get<Core>().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<Core>().mServiceTypes.FindMatching(mServiceType);
if (shouldAdd && (serviceType == nullptr))
{
serviceType = ServiceType::AllocateAndInit(GetInstance(), mServiceType.AsCString());
OT_ASSERT(serviceType != nullptr);
Get<Core>().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<Core>().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);
ResourceRecord::UpdateRecordLengthInMessage(*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);
ResourceRecord::UpdateRecordLengthInMessage(*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<ServiceEntry &>(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<Core>().mInfraIfIndex;
aState = static_cast<EntryState>(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<Core>().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<Core>().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<Core>().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);
ResourceRecord::UpdateRecordLengthInMessage(*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);
mUnicastDest.Clear();
}
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<MessagePool>().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<MessagePool>().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<Core>().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<Core>().mTxMessageHistory.Add(*mMsgPtr);
LogVerbose("Sending %s message len:%u", TypeToString(mType), mMsgPtr->GetLength());
if (!mUnicastDest.GetAddress().IsUnspecified())
{
LogVerbose(" dst: %s", mUnicastDest.ToString().AsCString());
}
Get<Core>().LogMessage(*mMsgPtr);
// We pass ownership of message to the platform layer.
switch (mType)
{
case kMulticastProbe:
case kMulticastQuery:
case kMulticastResponse:
otPlatMdnsSendMulticast(&GetInstance(), mMsgPtr.Release(), Get<Core>().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<Core>().mHostEntries)
{
if (ShouldClearAppendStateOnReinit(entry))
{
entry.ClearAppendState();
}
}
for (ServiceEntry &entry : Get<Core>().mServiceEntries)
{
if (ShouldClearAppendStateOnReinit(entry))
{
entry.ClearAppendState();
}
}
for (ServiceType &serviceType : Get<Core>().mServiceTypes)
{
if ((GetType() == kMulticastResponse) || (GetType() == kUnicastResponse))
{
serviceType.ClearAppendState();
}
}
break;
case kMulticastQuery:
for (BrowseCache &browseCache : Get<Core>().mBrowseCacheList)
{
browseCache.ClearCompressOffsets();
}
for (SrvCache &srvCache : Get<Core>().mSrvCacheList)
{
srvCache.ClearCompressOffsets();
}
for (TxtCache &txtCache : Get<Core>().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;
}
const char *Core::TxMessage::TypeToString(Type aType)
{
#define TypeMapList(_) \
_(kMulticastProbe, "multicast probe") \
_(kMulticastQuery, "multicast query") \
_(kMulticastResponse, "multicast response") \
_(kUnicastResponse, "unicast response") \
_(kLegacyUnicastResponse, "legacy-unicast response")
DefineEnumStringArray(TypeMapList);
return kStrings[aType];
}
//----------------------------------------------------------------------------------------------------------------------
// 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<Message> &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);
mIsSelfOriginating = Get<Core>().mTxMessageHistory.Contains(*aMessagePtr);
if (mIsSelfOriginating)
{
LogVerbose("Received message len:%u (self-originated)", aMessagePtr->GetLength());
}
else
{
LogVerbose("Received message len:%u", aMessagePtr->GetLength());
LogVerbose(" sender:%s", mSenderAddress.ToString().AsCString());
Get<Core>().LogMessage(*aMessagePtr);
}
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;
SuccessOrExit(error = 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;
}
}
mMessagePtr = aMessagePtr.PassOwnership();
exit:
LogInfoOnError(error, "parse message from %s", aSenderAddress.GetAddress().ToString().AsCString());
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::GenerateInClosedRange(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<Core>().mServiceTypes.IsEmpty());
aQuestion.mCanAnswer = true;
aQuestion.mIsForAllServicesDnssd = true;
ExitNow();
}
// Check if question name matches a `HostEntry` or a `ServiceEntry`.
aQuestion.mEntry = Get<Core>().mHostEntries.FindMatching(name);
if (aQuestion.mEntry == nullptr)
{
aQuestion.mEntry = Get<Core>().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<Core>().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<HostEntry *>(aQuestion.mEntry);
serviceEntry = aQuestion.mIsForService ? static_cast<ServiceEntry *>(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<Core>().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<Core>().mHostEntries)
{
entry.ClearAppendState();
entry.PrepareResponse(context);
}
for (ServiceEntry &entry : Get<Core>().mServiceEntries)
{
entry.ClearAppendState();
entry.PrepareResponse(context);
}
for (ServiceType &serviceType : Get<Core>().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<Core>().mBrowseCacheList.IsEmpty())
{
IterateOnAllRecordsInResponse(&RxMessage::ProcessPtrRecord);
}
if (!Get<Core>().mSrvCacheList.IsEmpty())
{
IterateOnAllRecordsInResponse(&RxMessage::ProcessSrvRecord);
}
if (!Get<Core>().mTxtCacheList.IsEmpty())
{
IterateOnAllRecordsInResponse(&RxMessage::ProcessTxtRecord);
}
if (!Get<Core>().mIp6AddrCacheList.IsEmpty())
{
IterateOnAllRecordsInResponse(&RxMessage::ProcessAaaaRecord);
for (Ip6AddrCache &addrCache : Get<Core>().mIp6AddrCacheList)
{
addrCache.CommitNewResponseEntries();
}
}
if (!Get<Core>().mIp4AddrCacheList.IsEmpty())
{
IterateOnAllRecordsInResponse(&RxMessage::ProcessARecord);
for (Ip4AddrCache &addrCache : Get<Core>().mIp4AddrCacheList)
{
addrCache.CommitNewResponseEntries();
}
}
if (!Get<Core>().mRecordCacheList.IsEmpty())
{
IterateOnAllRecordsInResponse(&RxMessage::ProcessOtherRecord);
for (RecordCache &recordCache : Get<Core>().mRecordCacheList)
{
recordCache.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<uint16_t>(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<Core>().mHostEntries.FindMatching(aName);
if (hostEntry != nullptr)
{
hostEntry->HandleConflict();
}
serviceEntry = Get<Core>().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<Core>().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<Core>().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<Core>().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<Core>().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<Core>().mIp4AddrCacheList.FindMatching(aName);
VerifyOrExit(ip4AddrCache != nullptr);
ip4AddrCache->ProcessResponseRecord(*mMessagePtr, aRecordOffset);
exit:
return;
}
void Core::RxMessage::ProcessOtherRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset)
{
// Unlike other `Process{Specific}Record()` methods where
// we know for sure that we can have only one match, for
// `RecordQuerier` we may have multiple matches, due to
// the possibility of using `ANY` for record type.
for (RecordCache &recordCache : Get<Core>().mRecordCacheList)
{
if (recordCache.Matches(aName, aRecord.GetType()))
{
recordCache.ProcessResponseRecord(*mMessagePtr, aRecord, aRecordOffset);
}
}
}
//---------------------------------------------------------------------------------------------------------------------
// 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<RxMessage> &aRxMessagePtr)
{
RxMsgEntry *msgEntry = mRxMsgEntries.FindMatching(aRxMessagePtr->GetSenderAddress());
VerifyOrExit(msgEntry != nullptr);
msgEntry->Add(aRxMessagePtr);
exit:
return;
}
void Core::MultiPacketRxMessages::AddNew(OwnedPtr<RxMessage> &aRxMessagePtr)
{
RxMsgEntry *newEntry;
// First remove existing entries matching same sender
// before adding the new entry to the list.
mRxMsgEntries.RemoveMatching(aRxMessagePtr->GetSenderAddress());
VerifyOrExit(mRxMsgEntries.CountAllEntries() < kMaxRxMsgEntries);
newEntry = RxMsgEntry::Allocate(GetInstance());
VerifyOrExit(newEntry != nullptr);
newEntry->Add(aRxMessagePtr);
mRxMsgEntries.Push(*newEntry);
exit:
return;
}
void Core::MultiPacketRxMessages::HandleTimer(void)
{
NextFireTime nextTime;
OwningList<RxMsgEntry> expiredEntries;
mRxMsgEntries.RemoveAllMatching(expiredEntries, ExpirationChecker(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;
}
void Core::MultiPacketRxMessages::RxMsgEntry::Add(OwnedPtr<RxMessage> &aRxMessagePtr)
{
// 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.
VerifyOrExit(mRxMessages.CountAllEntries() < kMaxNumMessagesPerEntry);
mProcessTime = TimerMilli::GetNow();
if (aRxMessagePtr->IsTruncated())
{
mProcessTime += Random::NonCrypto::GenerateInClosedRange(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<Core>().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<uint16_t>(kCrc16AnsiPolynomial).Feed(aMessage, offsetRange);
mCrc32 = CrcCalculator<uint32_t>(kCrc32AnsiPolynomial).Feed(aMessage, offsetRange);
}
void Core::TxMessageHistory::HandleTimer(void)
{
NextFireTime nextTime;
mMsgEntries.RemoveAndFreeAllMatching(ExpirationChecker(nextTime.GetNow()));
for (const MsgEntry &entry : mMsgEntries)
{
nextTime.UpdateIfEarlier(entry.mExpireTime);
}
mTimer.FireAtIfEarlier(nextTime);
}
//---------------------------------------------------------------------------------------------------------------------
// Core
template <typename CacheType, typename BrowserResolverType>
Error Core::Start(const BrowserResolverType &aBrowserOrResolver)
{
Error error = kErrorNone;
CacheType *cacheEntry;
VerifyOrExit(mIsEnabled, error = kErrorInvalidState);
VerifyOrExit(aBrowserOrResolver.mCallback != nullptr, error = kErrorInvalidArgs);
SuccessOrExit(error = ValidateNamesIn(aBrowserOrResolver));
cacheEntry = GetCacheList<CacheType>().FindMatching(aBrowserOrResolver);
if (cacheEntry == nullptr)
{
cacheEntry = CacheType::AllocateAndInit(GetInstance(), aBrowserOrResolver);
OT_ASSERT(cacheEntry != nullptr);
GetCacheList<CacheType>().Push(*cacheEntry);
}
error = cacheEntry->Add(aBrowserOrResolver);
exit:
return error;
}
template <typename CacheType, typename BrowserResolverType>
Error Core::Stop(const BrowserResolverType &aBrowserOrResolver)
{
Error error = kErrorNone;
CacheType *cacheEntry;
VerifyOrExit(mIsEnabled, error = kErrorInvalidState);
VerifyOrExit(aBrowserOrResolver.mCallback != nullptr, error = kErrorInvalidArgs);
SuccessOrExit(error = ValidateNamesIn(aBrowserOrResolver));
cacheEntry = GetCacheList<CacheType>().FindMatching(aBrowserOrResolver);
VerifyOrExit(cacheEntry != nullptr);
cacheEntry->Remove(aBrowserOrResolver);
exit:
return error;
}
Error Core::StartBrowser(const Browser &aBrowser) { return Start<BrowseCache, Browser>(aBrowser); }
Error Core::StopBrowser(const Browser &aBrowser) { return Stop<BrowseCache, Browser>(aBrowser); }
Error Core::StartSrvResolver(const SrvResolver &aResolver) { return Start<SrvCache, SrvResolver>(aResolver); }
Error Core::StopSrvResolver(const SrvResolver &aResolver) { return Stop<SrvCache, SrvResolver>(aResolver); }
Error Core::StartTxtResolver(const TxtResolver &aResolver) { return Start<TxtCache, TxtResolver>(aResolver); }
Error Core::StopTxtResolver(const TxtResolver &aResolver) { return Stop<TxtCache, TxtResolver>(aResolver); }
Error Core::StartIp6AddressResolver(const AddressResolver &aResolver)
{
return Start<Ip6AddrCache, AddressResolver>(aResolver);
}
Error Core::StartRecordQuerier(const RecordQuerier &aQuerier)
{
Error error;
switch (aQuerier.mRecordType)
{
case ResourceRecord::kTypePtr:
case ResourceRecord::kTypeSrv:
case ResourceRecord::kTypeTxt:
case ResourceRecord::kTypeAaaa:
case ResourceRecord::kTypeA:
error = kErrorInvalidArgs;
break;
default:
error = Start<RecordCache, RecordQuerier>(aQuerier);
break;
}
return error;
}
Error Core::StopRecordQuerier(const RecordQuerier &aQuerier) { return Stop<RecordCache, RecordQuerier>(aQuerier); }
Error Core::StopIp6AddressResolver(const AddressResolver &aResolver)
{
return Stop<Ip6AddrCache, AddressResolver>(aResolver);
}
Error Core::StartIp4AddressResolver(const AddressResolver &aResolver)
{
return Start<Ip4AddrCache, AddressResolver>(aResolver);
}
Error Core::StopIp4AddressResolver(const AddressResolver &aResolver)
{
return Stop<Ip4AddrCache, AddressResolver>(aResolver);
}
Error Core::ValidateNamesIn(const Browser &aBrowser) const
{
Error error;
SuccessOrExit(error = Name::ValidateName(aBrowser.mServiceType));
if (aBrowser.mSubTypeLabel != nullptr)
{
error = Name::ValidateLabel(aBrowser.mSubTypeLabel);
}
exit:
return error;
}
Error Core::ValidateNamesIn(const SrvResolver &aSrvResolver) const
{
Error error;
SuccessOrExit(error = Name::ValidateLabel(aSrvResolver.mServiceInstance));
error = Name::ValidateName(aSrvResolver.mServiceType);
exit:
return error;
}
Error Core::ValidateNamesIn(const TxtResolver &aTxtResolver) const
{
Error error;
SuccessOrExit(error = Name::ValidateLabel(aTxtResolver.mServiceInstance));
error = Name::ValidateName(aTxtResolver.mServiceType);
exit:
return error;
}
Error Core::ValidateNamesIn(const AddressResolver &aAddressResolver) const
{
return Name::ValidateName(aAddressResolver.mHostName);
}
Error Core::ValidateNamesIn(const RecordQuerier &aRecordQuerier) const
{
Error error;
SuccessOrExit(error = Name::ValidateLabel(aRecordQuerier.mFirstLabel));
if (aRecordQuerier.mNextLabels != nullptr)
{
error = Name::ValidateName(aRecordQuerier.mNextLabels);
}
exit:
return error;
}
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());
ExpirationChecker expirationChecker(context.GetNow());
// First remove all expired entries.
mBrowseCacheList.RemoveAndFreeAllMatching(expirationChecker);
mSrvCacheList.RemoveAndFreeAllMatching(expirationChecker);
mTxtCacheList.RemoveAndFreeAllMatching(expirationChecker);
mIp6AddrCacheList.RemoveAndFreeAllMatching(expirationChecker);
mIp4AddrCacheList.RemoveAndFreeAllMatching(expirationChecker);
mRecordCacheList.RemoveAndFreeAllMatching(expirationChecker);
// 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);
}
for (RecordCache &recordCache : mRecordCacheList)
{
recordCache.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();
}
for (RecordCache &recordCache : mRecordCacheList)
{
recordCache.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::GenerateInClosedRange(kMinProbeDelay, kMaxProbeDelay);
}
return mNextProbeTxTime;
}
TimeMilli Core::RandomizeInitialQueryTxTime(void)
{
TimeMilli now = TimerMilli::GetNow();
if ((mNextQueryTxTime - now) >= kMaxInitialQueryDelay)
{
mNextQueryTxTime = now + Random::NonCrypto::GenerateInClosedRange(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);
}
}
void Core::ResultCallback::Invoke(Instance &aInstance, const RecordResult &aResult) const
{
if (mSharedCallback.mRecord != nullptr)
{
mSharedCallback.mRecord(&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::GenerateUpToExcluding(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;
mContinuousRetry = false;
mQueryPending = false;
mLastQueryTimeValid = false;
mIsActive = false;
mDeleteTime = TimerMilli::GetNow() + kNonActiveDeleteTimeout;
mRetryInterval = 0;
mJitteredRetryInterval = 0;
}
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 the "delete timeout"
// if no resolver/browser/querier is added.
mIsActive = aIsActive;
if (!mIsActive)
{
mQueryPending = false;
mDeleteTime = TimerMilli::GetNow() + DetermineDeleteTimeout();
SetFireTime(mDeleteTime);
}
}
uint32_t Core::CacheEntry::DetermineDeleteTimeout(void) const
{
uint32_t timeout = kNonActiveDeleteTimeout;
if ((mType == kRecordCache) && (As<RecordCache>().mRecordType == ResourceRecord::kTypeAny))
{
timeout = kNonActiveDeleteTimeoutForAnyRecord;
}
return timeout;
}
bool Core::CacheEntry::ShouldDelete(TimeMilli aNow) const { return !mIsActive && (mDeleteTime <= aNow); }
void Core::CacheEntry::StartInitialQueries(void)
{
mContinuousRetry = true;
mRetryInterval = 0;
mJitteredRetryInterval = 0;
mLastQueryTimeValid = false;
mLastQueryTime = Get<Core>().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:
case kRecordCache:
shouldStart = true;
break;
case kSrvCache:
case kTxtCache:
shouldStart = As<ServiceCache>().ShouldStartInitialQueries();
break;
case kIp6AddrCache:
case kIp4AddrCache:
shouldStart = As<AddrCache>().ShouldStartInitialQueries();
break;
}
if (shouldStart)
{
StartInitialQueries();
}
DetermineNextFireTime();
ScheduleTimer();
}
// Report any discovered/cached result to the newly added
// callback.
switch (mType)
{
case kBrowseCache:
As<BrowseCache>().ReportResultsTo(*callback);
break;
case kSrvCache:
As<SrvCache>().ReportResultTo(*callback);
break;
case kTxtCache:
As<TxtCache>().ReportResultTo(*callback);
break;
case kIp6AddrCache:
case kIp4AddrCache:
As<AddrCache>().ReportResultsTo(*callback);
break;
case kRecordCache:
As<RecordCache>().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<Core>().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<BrowseCache>().ClearCompressOffsets();
break;
case kSrvCache:
case kTxtCache:
As<ServiceCache>().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;
case kRecordCache:
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<BrowseCache>().ProcessExpiredRecords(aContext.GetNow());
break;
case kSrvCache:
As<SrvCache>().ProcessExpiredRecords(aContext.GetNow());
break;
case kTxtCache:
As<TxtCache>().ProcessExpiredRecords(aContext.GetNow());
break;
case kIp6AddrCache:
case kIp4AddrCache:
As<AddrCache>().ProcessExpiredRecords(aContext.GetNow());
break;
case kRecordCache:
As<RecordCache>().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;
case kRecordCache:
callback = mCallbacks.FindMatching(aCallback.mSharedCallback.mRecord);
break;
}
return callback;
}
void Core::CacheEntry::DetermineNextFireTime(void)
{
mQueryPending = false;
if (mContinuousRetry)
{
ScheduleQuery(mLastQueryTime + mJitteredRetryInterval);
}
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<BrowseCache>().DetermineRecordFireTime();
break;
case kSrvCache:
case kTxtCache:
As<ServiceCache>().DetermineRecordFireTime();
break;
case kIp6AddrCache:
case kIp4AddrCache:
As<AddrCache>().DetermineRecordFireTime();
break;
case kRecordCache:
As<RecordCache>().DetermineRecordFireTime();
break;
}
}
void Core::CacheEntry::UpdateQueryRetryInterval(void)
{
uint16_t maxJitter;
VerifyOrExit(mContinuousRetry);
mRetryInterval *= kQueryRetryGrowthFactor;
mRetryInterval = Clamp(mRetryInterval, kMinQueryRetryInterval, kMaxQueryRetryInterval);
// We pre-calculate the jittered retry interval to ensure
// `DetermineNextFireTime()` uses a consistent value.
maxJitter = ClampToUint16(mRetryInterval / kQueryRetryJitterDivisor);
mJitteredRetryInterval = Random::NonCrypto::AddJitter(mRetryInterval, maxJitter);
exit:
return;
}
void Core::CacheEntry::ScheduleTimer(void) { ScheduleFireTimeOn(Get<Core>().mCacheTimer); }
void Core::CacheEntry::PrepareQuery(CacheContext &aContext)
{
bool prepareAgain = false;
do
{
TxMessage &query = aContext.mQueryMessage;
query.SaveCurrentState();
switch (mType)
{
case kBrowseCache:
As<BrowseCache>().PreparePtrQuestion(query, aContext.GetNow());
break;
case kSrvCache:
As<SrvCache>().PrepareSrvQuestion(query);
break;
case kTxtCache:
As<TxtCache>().PrepareTxtQuestion(query);
break;
case kIp6AddrCache:
As<Ip6AddrCache>().PrepareAaaaQuestion(query);
break;
case kIp4AddrCache:
As<Ip4AddrCache>().PrepareAQuestion(query);
break;
case kRecordCache:
As<RecordCache>().PrepareQueryQuestion(query);
break;
}
query.CheckSizeLimitToPrepareAgain(prepareAgain);
} while (prepareAgain);
mLastQueryTimeValid = true;
mLastQueryTime = aContext.GetNow();
UpdateQueryRetryInterval();
// Let the cache entry super-classes update their state
// after query was sent.
switch (mType)
{
case kBrowseCache:
As<BrowseCache>().UpdateRecordStateAfterQuery(aContext.GetNow());
break;
case kSrvCache:
case kTxtCache:
As<ServiceCache>().UpdateRecordStateAfterQuery(aContext.GetNow());
break;
case kIp6AddrCache:
case kIp4AddrCache:
As<AddrCache>().UpdateRecordStateAfterQuery(aContext.GetNow());
break;
case kRecordCache:
As<RecordCache>().UpdateRecordStateAfterQuery(aContext.GetNow());
}
}
template <typename ResultType> 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 ExpirationChecker &aChecker) const { return ShouldDelete(aChecker.GetNow()); }
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<Core>().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<Core>().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<Core>().mSrvCacheList)
{
if (NameMatch(srvCache.mServiceType, mServiceType))
{
UpdateCompressOffset(mServiceTypeOffset, srvCache.mServiceTypeOffset);
VerifyOrExit(mServiceTypeOffset == kUnspecifiedOffset);
}
}
for (const TxtCache &txtCache : Get<Core>().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);
ResourceRecord::UpdateRecordLengthInMessage(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<PtrEntry> expiredEntries;
mPtrEntries.RemoveAllMatching(expiredEntries, ExpirationChecker(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<Core>().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 ExpirationChecker &aChecker) const
{
return mRecord.ShouldExpire(aChecker.GetNow());
}
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<Core>().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 ExpirationChecker &aChecker) const { return ShouldDelete(aChecker.GetNow()); }
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())
{
StopQueryRetries();
// If not present already, we add a passive `TxtCache` for the
// same service name, and an `Ip6AddrCache` for the host name.
Get<Core>().AddPassiveSrvTxtCache(mServiceInstance.AsCString(), mServiceType.AsCString());
Get<Core>().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<Core>().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<Core>().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<Core>().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 ExpirationChecker &aChecker) const { return ShouldDelete(aChecker.GetNow()); }
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())
{
StopQueryRetries();
}
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<Core>().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<Core>().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<Core>().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<Core>().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 ExpirationChecker &aChecker) const { return ShouldDelete(aChecker.GetNow()); }
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<AddressAndTtl> addrArray;
AddressResult result;
bool didRemoveAny;
didRemoveAny = mCommittedEntries.RemoveAndFreeAllMatching(ExpirationChecker(aNow));
VerifyOrExit(didRemoveAny);
ConstructResult(result, addrArray);
InvokeCallbacks(result);
exit:
return;
}
void Core::AddrCache::ReportResultsTo(ResultCallback &aCallback) const
{
Heap::Array<AddressAndTtl> addrArray;
AddressResult result;
ConstructResult(result, addrArray);
if (result.mAddressesLength > 0)
{
aCallback.Invoke(GetInstance(), result);
}
}
void Core::AddrCache::ConstructResult(AddressResult &aResult, Heap::Array<AddressAndTtl> &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<Core>().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<AddrEntry> 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());
}
}
StopQueryRetries();
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Invoke callbacks if there is any change.
if (changed)
{
Heap::Array<AddressAndTtl> 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<Core>().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 ExpirationChecker &aChecker) const
{
return mRecord.ShouldExpire(aChecker.GetNow());
}
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::RecordCache
Error Core::RecordCache::Init(Instance &aInstance, const RecordQuerier &aQuerier)
{
Error error;
CacheEntry::Init(aInstance, kRecordCache);
mNext = nullptr;
SuccessOrExit(error = mFirstLabel.Set(aQuerier.mFirstLabel));
SuccessOrExit(error = mNextLabels.Set(aQuerier.mNextLabels));
mRecordType = aQuerier.mRecordType;
exit:
return error;
}
bool Core::RecordCache::Matches(const Name &aFullName, uint16_t aRecordType) const
{
return QuestionMatches(mRecordType, aRecordType) &&
aFullName.Matches(mFirstLabel.AsCString(), mNextLabels.AsCString(), kLocalDomain);
}
bool Core::RecordCache::Matches(const RecordQuerier &aQuerier) const
{
bool matches = false;
VerifyOrExit(aQuerier.mRecordType == mRecordType);
VerifyOrExit(NameMatch(mFirstLabel, aQuerier.mFirstLabel));
if (mNextLabels.IsNull())
{
VerifyOrExit(aQuerier.mNextLabels == nullptr);
}
else
{
VerifyOrExit(NameMatch(mNextLabels, aQuerier.mNextLabels));
}
matches = true;
exit:
return matches;
}
bool Core::RecordCache::Matches(const ExpirationChecker &aChecker) const { return ShouldDelete(aChecker.GetNow()); }
Error Core::RecordCache::Add(const RecordQuerier &aQuerier)
{
return CacheEntry::Add(ResultCallback(aQuerier.mCallback));
}
void Core::RecordCache::Remove(const RecordQuerier &aQuerier)
{
return CacheEntry::Remove(ResultCallback(aQuerier.mCallback));
}
void Core::RecordCache::PrepareQueryQuestion(TxMessage &aQuery)
{
Question question;
question.SetType(mRecordType);
question.SetClass(ResourceRecord::kClassInternet);
AppendNameTo(aQuery, kQuestionSection);
SuccessOrAssert(aQuery.SelectMessageFor(kQuestionSection).Append(question));
aQuery.IncrementRecordCount(kQuestionSection);
}
void Core::RecordCache::AppendNameTo(TxMessage &aTxMessage, Section aSection)
{
uint16_t compressOffset = kUnspecifiedOffset;
AppendOutcome outcome;
outcome = aTxMessage.AppendLabel(aSection, mFirstLabel.AsCString(), compressOffset);
VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
if (!mNextLabels.IsNull())
{
compressOffset = kUnspecifiedOffset;
outcome = aTxMessage.AppendMultipleLabels(aSection, mNextLabels.AsCString(), compressOffset);
VerifyOrExit(outcome != kAppendedFullNameAsCompressed);
}
aTxMessage.AppendDomainName(aSection);
exit:
return;
}
void Core::RecordCache::UpdateRecordStateAfterQuery(TimeMilli aNow)
{
for (RecordEntry &entry : mCommittedEntries)
{
entry.mRecord.UpdateStateAfterQuery(aNow);
}
}
void Core::RecordCache::ProcessResponseRecord(const Message &aMessage,
const ResourceRecord &aRecord,
uint16_t aRecordOffset)
{
// Name and record type in `aMessage` are already matched.
// First, checks if the record data needs to be decompressed
// (the record data format can contain one or more compressed DNS
// names). This check applies to records: NS, CNAME, SOA, PTR,
// MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, and NSEC.
//
// Then, adds the new record data to the `mNewEntries` list. This
// step occurs as the records in a received response are
// processed one by one. Once all records are processed,
// `CommitNewResponseEntries()` is called to update the list.
OwnedPtr<Message> dataMsg;
Heap::Data data;
NewRecordEntry *entry;
SuccessOrExit(ResourceRecord::DecompressRecordData(aMessage, aRecordOffset, dataMsg));
if (dataMsg != nullptr)
{
SuccessOrAssert(data.SetFrom(*dataMsg));
}
else
{
SuccessOrAssert(data.SetFrom(aMessage, aRecordOffset + sizeof(ResourceRecord), aRecord.GetLength()));
}
// Check for duplicates in the same response. If there
// are exact duplicates, we remember the last one in the
// response message.
entry = mNewEntries.FindMatching(aRecord.GetType(), data);
if (entry != nullptr)
{
entry->mCacheFlush = (aRecord.GetClass() & kClassCacheFlushFlag);
entry->mTtl = aRecord.GetTtl();
}
else
{
entry = NewRecordEntry::Allocate(aRecord, data);
OT_ASSERT(entry != nullptr);
mNewEntries.Push(*entry);
}
exit:
return;
}
void Core::RecordCache::CommitNewResponseEntries(void)
{
// If `RecordQuerier` is used for record type ANY, multiple new
// records with different types may be included in the received
// response. We process and commit all the new records matching
// the same type, together.
while (!mNewEntries.IsEmpty())
{
uint16_t recordType = mNewEntries.GetHead()->mType;
CommitNewEntriesForType(recordType);
}
mCommittedEntries.RemoveAndFreeAllMatching(EmptyChecker());
DetermineNextFireTime();
ScheduleTimer();
}
void Core::RecordCache::CommitNewEntriesForType(uint16_t aRecordType)
{
bool shouldFlush = false;
OwningList<NewRecordEntry> newMatchingEntries;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Filter and remove all new entries that match `aRecordType`.
mNewEntries.RemoveAllMatching(newMatchingEntries, aRecordType);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Determine whether we should flush cache for previously
// committed records of `aRecordType`.
for (const NewRecordEntry &newEntry : newMatchingEntries)
{
if (newEntry.mCacheFlush)
{
shouldFlush = true;
break;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Invoke callbacks if there is any change.
// If we need to flush, check any entry in the previously
// `mCommittedEntries` that does not appear in the new list
// and signal their removal.
if (shouldFlush)
{
for (RecordEntry &entry : mCommittedEntries)
{
if (!entry.Matches(aRecordType))
{
continue;
}
if (!newMatchingEntries.ContainsMatching(entry.mType, entry.mData))
{
entry.mRecord.RefreshTtl(0);
PrepareResultAndInvokeCallbacks(entry);
}
}
}
// Signal addition of any new entries or if there is any
// change to an existing entry (TTL value changed).
for (const NewRecordEntry &newEntry : newMatchingEntries)
{
RecordEntry *entry = mCommittedEntries.FindMatching(newEntry.mType, newEntry.mData);
bool shouldSignal = false;
if (entry == nullptr)
{
shouldSignal = (newEntry.mTtl > 0);
}
else
{
shouldSignal = (entry->GetTtl() != newEntry.mTtl);
}
if (shouldSignal)
{
PrepareResultAndInvokeCallbacks(newEntry);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Now merge the new entries into the `mCommittedEntries` list.
if (shouldFlush)
{
mCommittedEntries.RemoveAndFreeAllMatching(aRecordType);
if (mRecordType != ResourceRecord::kTypeAny)
{
StopQueryRetries();
}
}
while (!newMatchingEntries.IsEmpty())
{
OwnedPtr<NewRecordEntry> newEntry = newMatchingEntries.Pop();
RecordEntry *entry;
entry = mCommittedEntries.FindMatching(newEntry->mType, newEntry->mData);
if (entry != nullptr)
{
entry->mRecord.RefreshTtl(newEntry->mTtl);
}
else
{
entry = RecordEntry::Allocate(*newEntry);
OT_ASSERT(entry != nullptr);
mCommittedEntries.Push(*entry);
}
}
}
void Core::RecordCache::DetermineRecordFireTime(void)
{
for (RecordEntry &entry : mCommittedEntries)
{
entry.mRecord.UpdateQueryAndFireTimeOn(*this);
}
}
void Core::RecordCache::ProcessExpiredRecords(TimeMilli aNow)
{
OwningList<RecordEntry> expiredEntries;
mCommittedEntries.RemoveAllMatching(expiredEntries, ExpirationChecker(aNow));
for (RecordEntry &entry : expiredEntries)
{
entry.mRecord.RefreshTtl(0);
PrepareResultAndInvokeCallbacks(entry);
}
}
void Core::RecordCache::ReportResultsTo(ResultCallback &aCallback) const
{
for (const RecordEntry &entry : mCommittedEntries)
{
RecordResult result;
PreareResultFor(entry.mType, entry.mData, entry.GetTtl(), result);
aCallback.Invoke(GetInstance(), result);
}
}
void Core::RecordCache::PreareResultFor(uint16_t aType,
const Heap::Data &aData,
uint32_t aTtl,
RecordResult &aResult) const
{
ClearAllBytes(aResult);
aResult.mFirstLabel = mFirstLabel.AsCString();
aResult.mNextLabels = mNextLabels.AsCString();
aResult.mRecordType = aType;
aResult.mRecordData = aData.GetBytes();
aResult.mRecordDataLength = aData.GetLength();
aResult.mTtl = aTtl;
aResult.mInfraIfIndex = Get<Core>().mInfraIfIndex;
}
void Core::RecordCache::PrepareResultAndInvokeCallbacks(const RecordEntry &aEntry)
{
PrepareResultAndInvokeCallbacks(aEntry.mType, aEntry.mData, aEntry.GetTtl());
}
void Core::RecordCache::PrepareResultAndInvokeCallbacks(const NewRecordEntry &aNewEntry)
{
PrepareResultAndInvokeCallbacks(aNewEntry.mType, aNewEntry.mData, aNewEntry.mTtl);
}
void Core::RecordCache::PrepareResultAndInvokeCallbacks(uint16_t aType, const Heap::Data &aData, uint32_t aTtl)
{
RecordResult result;
PreareResultFor(aType, aData, aTtl, result);
InvokeCallbacks(result);
}
#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE
void Core::RecordCache::CopyInfoTo(RecordQuerier &aQuerier, CacheInfo &aInfo) const
{
aQuerier.mFirstLabel = mFirstLabel.AsCString();
aQuerier.mNextLabels = mNextLabels.AsCString();
aQuerier.mRecordType = mRecordType;
aQuerier.mInfraIfIndex = Get<Core>().mInfraIfIndex;
aQuerier.mCallback = nullptr;
aInfo.mIsActive = IsActive();
aInfo.mHasCachedResults = !mCommittedEntries.IsEmpty();
}
#endif
//---------------------------------------------------------------------------------------------------------------------
// Core::RecordCache::NewRecordEntry
Core::RecordCache::NewRecordEntry::NewRecordEntry(const ResourceRecord &aRecord, Heap::Data &aData)
: mNext(nullptr)
, mCacheFlush(aRecord.GetClass() & kClassCacheFlushFlag)
, mType(aRecord.GetType())
, mTtl(aRecord.GetTtl())
, mData(aData.Move())
{
}
bool Core::RecordCache::NewRecordEntry::Matches(uint16_t aType) const { return (mType == aType); }
bool Core::RecordCache::NewRecordEntry::Matches(uint16_t aType, const Heap::Data &aData) const
{
return (mType == aType) && (aData == mData);
}
//---------------------------------------------------------------------------------------------------------------------
// Core::RecordCache::RecordEntry
Core::RecordCache::RecordEntry::RecordEntry(NewRecordEntry &aNewEntry)
: mNext(nullptr)
, mType(aNewEntry.mType)
, mData(aNewEntry.mData.Move())
{
mRecord.RefreshTtl(aNewEntry.mTtl);
}
bool Core::RecordCache::RecordEntry::Matches(uint16_t aType) const { return (mType == aType); }
bool Core::RecordCache::RecordEntry::Matches(uint16_t aType, const Heap::Data &aData) const
{
return (mType == aType) && (mData == aData);
}
bool Core::RecordCache::RecordEntry::Matches(const ExpirationChecker &aChecker) const
{
return mRecord.ShouldExpire(aChecker.GetNow());
}
bool Core::RecordCache::RecordEntry::Matches(EmptyChecker aChecker) const
{
OT_UNUSED_VARIABLE(aChecker);
return !mRecord.IsPresent();
}
//---------------------------------------------------------------------------------------------------------------------
// 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<Core>().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<Core>().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<Core>().mHostEntries.GetHead();
mType = kHostKey;
}
else
{
VerifyOrExit((mType == kServiceKey) || (mType == kHostKey), error = kErrorInvalidArgs);
}
while ((error == kErrorNotFound) && (mType == kHostKey))
{
if (mHostEntry == nullptr)
{
mServiceEntry = Get<Core>().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::GetNextLocalHostAddress(LocalHostAddress &aAddress)
{
Error error = kErrorNone;
uint16_t index;
Ip4::Address ip4Addr;
if (mType == kUnspecified)
{
mLocalHostAddrIndex = 0;
mType = kLocalHostAddress;
}
else
{
VerifyOrExit(mType == kLocalHostAddress, error = kErrorInvalidArgs);
}
ClearAllBytes(aAddress);
index = mLocalHostAddrIndex;
if (index < Get<Core>().mLocalHost.GetIp6Addresses().GetLength())
{
aAddress.mIsIp6 = true;
aAddress.mAddress.mIp6 = Get<Core>().mLocalHost.GetIp6Addresses()[index];
}
else
{
index -= Get<Core>().mLocalHost.GetIp6Addresses().GetLength();
VerifyOrExit(index < Get<Core>().mLocalHost.GetIp4Addresses().GetLength(), error = kErrorNotFound);
IgnoreError(ip4Addr.ExtractFromIp4MappedIp6Address(Get<Core>().mLocalHost.GetIp4Addresses()[index]));
aAddress.mIsIp6 = false;
aAddress.mAddress.mIp4 = ip4Addr;
}
aAddress.mInfraIfIndex = Get<Core>().mInfraIfIndex;
mLocalHostAddrIndex++;
exit:
return error;
}
Error Core::EntryIterator::GetNextBrowser(Browser &aBrowser, CacheInfo &aInfo)
{
Error error = kErrorNone;
if (mType == kUnspecified)
{
mBrowseCache = Get<Core>().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<Core>().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<Core>().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<Core>().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<Core>().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;
}
Error Core::EntryIterator::GetNextRecordQuerier(RecordQuerier &aQuerier, CacheInfo &aInfo)
{
Error error = kErrorNone;
if (mType == kUnspecified)
{
mRecordCache = Get<Core>().mRecordCacheList.GetHead();
mType = kRecordQuerier;
}
else
{
VerifyOrExit(mType == kRecordQuerier, error = kErrorInvalidArgs);
}
VerifyOrExit(mRecordCache != nullptr, error = kErrorNotFound);
mRecordCache->CopyInfoTo(aQuerier, aInfo);
mRecordCache = mRecordCache->GetNext();
exit:
return error;
}
#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE
//---------------------------------------------------------------------------------------------------------------------
// Core::MsgLogger
#if OPENTHREAD_CONFIG_MULTICAST_DNS_VERBOSE_LOGGING_ENABLE
Core::MsgLogger::MsgLogger(Instance &aInstance, const Message &aMessage)
: InstanceLocator(aInstance)
, mMessage(aMessage)
{
}
void Core::MsgLogger::Log(void)
{
Error error = kErrorNone;
mOffset = mMessage.GetOffset();
SuccessOrExit(error = mMessage.Read(mOffset, mHeader));
mOffset += sizeof(Header);
LogVerbose("- %s id:%u qt:%u t:%u rcode:%u [q:%u ans:%u auth:%u addn:%u]",
mHeader.GetType() == Header::kTypeQuery ? "Query" : "Response", mHeader.GetMessageId(),
mHeader.GetQueryType(), mHeader.IsTruncationFlagSet(), mHeader.GetResponseCode(),
mHeader.GetQuestionCount(), mHeader.GetAnswerCount(), mHeader.GetAuthorityRecordCount(),
mHeader.GetAdditionalRecordCount());
SuccessOrExit(LogQuestions());
SuccessOrExit(error = LogSectionRecords("Answer", mHeader.GetAnswerCount()));
SuccessOrExit(error = LogSectionRecords("Authority", mHeader.GetAuthorityRecordCount()));
SuccessOrExit(error = LogSectionRecords("Additional", mHeader.GetAdditionalRecordCount()));
exit:
if (error != kErrorNone)
{
LogVerbose("Failed to parse message, error:%s", ErrorToString(error));
}
}
Error Core::MsgLogger::LogQuestions(void)
{
Error error = kErrorNone;
uint16_t questionCount = mHeader.GetQuestionCount();
VerifyOrExit(questionCount > 0);
LogVerbose("- Question");
for (; questionCount > 0; questionCount--)
{
Question question;
Name::Buffer name;
SuccessOrExit(error = Name::ReadName(mMessage, mOffset, name));
SuccessOrExit(error = mMessage.Read(mOffset, question));
mOffset += sizeof(Question);
LogVerbose(" %s", name);
LogVerbose(" %s %s class:%u", ResourceRecord::TypeToString(question.GetType()).AsCString(),
question.GetClass() & kClassQuestionUnicastFlag ? "QU" : "QM", question.GetClass() & kClassMask);
}
exit:
return error;
}
Error Core::MsgLogger::LogSectionRecords(const char *aSectionName, uint16_t aNumRecords)
{
Error error = kErrorNone;
VerifyOrExit(aNumRecords > 0);
LogVerbose("- %s", aSectionName);
for (; aNumRecords > 0; aNumRecords--)
{
SuccessOrExit(error = LogRecord());
}
exit:
return error;
}
Error Core::MsgLogger::LogRecord(void)
{
Error error = kErrorNone;
ResourceRecord record;
Name::Buffer name;
SuccessOrExit(error = Name::ReadName(mMessage, mOffset, name));
SuccessOrExit(error = mMessage.Read(mOffset, record));
mOffset += sizeof(ResourceRecord);
LogVerbose(" %s%s cls:%u ttl:%lu data-len:%u", ResourceRecord::TypeToString(record.GetType()).AsCString(),
record.GetClass() & kClassCacheFlushFlag ? " cache-flush" : "", record.GetClass() & kClassMask,
ToUlong(record.GetTtl()), record.GetLength());
LogVerbose(" %s", name);
LogRecordData(record);
mOffset += record.GetLength();
exit:
return error;
}
void Core::MsgLogger::LogRecordData(const ResourceRecord &aRecord)
{
uint16_t offset = mOffset;
Name::Buffer name;
Ip4::Address ip4Address;
Ip6::Address ip6Address;
SrvRecord srvRecord;
NsecRecord::TypeBitMap bitMap;
switch (aRecord.GetType())
{
case ResourceRecord::kTypeA:
VerifyOrExit(aRecord.GetLength() >= sizeof(Ip4::Address));
SuccessOrExit(mMessage.Read(offset, ip4Address));
LogVerbose(" %s", ip4Address.ToString().AsCString());
break;
case ResourceRecord::kTypeAaaa:
VerifyOrExit(aRecord.GetLength() >= sizeof(Ip6::Address));
SuccessOrExit(mMessage.Read(offset, ip6Address));
LogVerbose(" %s", ip6Address.ToString().AsCString());
break;
case ResourceRecord::kTypePtr:
SuccessOrExit(Name::ReadName(mMessage, offset, name));
LogVerbose(" %s", name);
break;
case ResourceRecord::kTypeSrv:
offset -= sizeof(ResourceRecord);
SuccessOrExit(mMessage.Read(offset, srvRecord));
offset += sizeof(srvRecord);
SuccessOrExit(Name::ReadName(mMessage, offset, name));
LogVerbose(" port:%u w:%u prio:%u", srvRecord.GetPort(), srvRecord.GetWeight(), srvRecord.GetPriority());
LogVerbose(" host:%s", name);
break;
case ResourceRecord::kTypeNsec:
SuccessOrExit(Name::ReadName(mMessage, offset, name));
LogVerbose(" domain-name:%s", name);
SuccessOrExit(mMessage.Read(offset, &bitMap, NsecRecord::TypeBitMap::kMinSize));
VerifyOrExit(bitMap.GetBlockNumber() == 0);
VerifyOrExit(bitMap.GetBitmapLength() <= NsecRecord::TypeBitMap::kMaxLength);
SuccessOrExit(mMessage.Read(offset, &bitMap, bitMap.GetSize()));
LogNsecBitMap(bitMap);
break;
case ResourceRecord::kTypeKey:
case ResourceRecord::kTypeTxt:
default:
LogRawData(aRecord.GetLength());
break;
}
exit:
return;
}
void Core::MsgLogger::LogRawData(uint16_t aLength)
{
static constexpr uint16_t kStringSize = 200;
static constexpr uint16_t kDataSize = 32;
uint16_t offset = mOffset;
bool isFirstLine = true;
while (aLength > 0)
{
uint16_t readLength = Min(aLength, kDataSize);
uint8_t data[kDataSize];
String<kStringSize> string;
if (isFirstLine)
{
string.Append("[ ");
isFirstLine = false;
}
else
{
string.Append(" ");
}
SuccessOrExit(mMessage.Read(offset, data, readLength));
string.AppendHexBytes(data, readLength);
offset += readLength;
aLength -= readLength;
if (aLength == 0)
{
string.Append(" ]");
}
LogVerbose(" %s", string.AsCString());
}
exit:
return;
}
void Core::MsgLogger::LogNsecBitMap(const NsecRecord::TypeBitMap &aBitMap)
{
static constexpr uint16_t kStringSize = 200;
String<kStringSize> string;
string.Append("[ ");
for (uint16_t type = 0; type < aBitMap.GetBitmapLength() * kBitsPerByte; type++)
{
if (aBitMap.ContainsType(type))
{
string.Append("%s ", ResourceRecord::TypeToString(type).AsCString());
}
}
string.Append("]");
LogVerbose(" %s", string.AsCString());
}
#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_VERBOSE_LOGGING_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