From ebccac6fbd6f65c29fe43f81614176a659c74123 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Tue, 25 Mar 2025 11:12:45 -0700 Subject: [PATCH] [mdns] enhance `RecordQuerier` to support ANY record type queries (#11364) This commit enhances `RecordQuerier` to support queries for the ANY record type. When querying for ANY, the response may contain various record types. The implementation ensures that these different types are cached separately and correctly handles responses containing multiple record types (with or without "cache-flush" flag). The `test_mdns` unit test is updated to validate this new behavior in detail. --- src/core/net/mdns.cpp | 250 +++++++++++++++------- src/core/net/mdns.hpp | 65 ++++-- tests/unit/test_mdns.cpp | 439 ++++++++++++++++++++++++++++++++++----- 3 files changed, 603 insertions(+), 151 deletions(-) diff --git a/src/core/net/mdns.cpp b/src/core/net/mdns.cpp index bd7774bf4..2d0303a4e 100644 --- a/src/core/net/mdns.cpp +++ b/src/core/net/mdns.cpp @@ -4243,15 +4243,18 @@ exit: void Core::RxMessage::ProcessOtherRecord(const Name &aName, const ResourceRecord &aRecord, uint16_t aRecordOffset) { - RecordCache *recordCache; + // 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. - recordCache = Get().mRecordCacheList.FindMatching(aName, aRecord.GetType()); - VerifyOrExit(recordCache != nullptr); - - recordCache->ProcessResponseRecord(*mMessagePtr, aRecord, aRecordOffset); - -exit: - return; + for (RecordCache &recordCache : Get().mRecordCacheList) + { + if (recordCache.Matches(aName, aRecord.GetType())) + { + recordCache.ProcessResponseRecord(*mMessagePtr, aRecord, aRecordOffset); + } + } } //--------------------------------------------------------------------------------------------------------------------- @@ -4911,19 +4914,31 @@ void Core::CacheEntry::SetIsActive(bool aIsActive) // considered "active" when associated with at least one // resolver/browser. "Passive" entries (without a resolver/browser) // continue to process mDNS responses for updates but will not send - // queries. Passive entries are deleted after `kNonActiveDeleteTimeout` - // if no resolver/browser is added. + // 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() + kNonActiveDeleteTimeout; + mDeleteTime = TimerMilli::GetNow() + DetermineDeleteTimeout(); SetFireTime(mDeleteTime); } } +uint32_t Core::CacheEntry::DetermineDeleteTimeout(void) const +{ + uint32_t timeout = kNonActiveDeleteTimeout; + + if ((mType == kRecordCache) && (As().mRecordType == ResourceRecord::kTypeAny)) + { + timeout = kNonActiveDeleteTimeoutForAnyRecord; + } + + return timeout; +} + bool Core::CacheEntry::ShouldDelete(TimeMilli aNow) const { return !mIsActive && (mDeleteTime <= aNow); } void Core::CacheEntry::StartInitialQueries(void) @@ -6509,8 +6524,7 @@ Error Core::RecordCache::Init(Instance &aInstance, const RecordQuerier &aQuerier CacheEntry::Init(aInstance, kRecordCache); - mNext = nullptr; - mShouldFlush = false; + mNext = nullptr; SuccessOrExit(error = mFirstLabel.Set(aQuerier.mFirstLabel)); SuccessOrExit(error = mNextLabels.Set(aQuerier.mNextLabels)); mRecordType = aQuerier.mRecordType; @@ -6521,7 +6535,7 @@ exit: bool Core::RecordCache::Matches(const Name &aFullName, uint16_t aRecordType) const { - return (mRecordType == aRecordType) && + return QuestionMatches(mRecordType, aRecordType) && aFullName.Matches(mFirstLabel.AsCString(), mNextLabels.AsCString(), kLocalDomain); } @@ -6529,7 +6543,7 @@ bool Core::RecordCache::Matches(const RecordQuerier &aQuerier) const { bool matches = false; - VerifyOrExit(mRecordType == aQuerier.mRecordType); + VerifyOrExit(aQuerier.mRecordType == mRecordType); VerifyOrExit(NameMatch(mFirstLabel, aQuerier.mFirstLabel)); @@ -6596,7 +6610,7 @@ exit: void Core::RecordCache::UpdateRecordStateAfterQuery(TimeMilli aNow) { - for (RecordDataEntry &entry : mCommittedEntries) + for (RecordEntry &entry : mCommittedEntries) { entry.mRecord.UpdateStateAfterQuery(aNow); } @@ -6613,35 +6627,74 @@ void Core::RecordCache::ProcessResponseRecord(const Message &aMessage, // Once all records are processed `CommitNewResponseEntries()` is // called to update the list. - Heap::Data data; - RecordDataEntry *entry; + Heap::Data data; + NewRecordEntry *entry; - SuccessOrExit(data.SetFrom(aMessage, aRecordOffset + sizeof(ResourceRecord), aRecord.GetLength())); + SuccessOrAssert(data.SetFrom(aMessage, aRecordOffset + sizeof(ResourceRecord), aRecord.GetLength())); - if (aRecord.GetClass() & kClassCacheFlushFlag) + // 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) { - mShouldFlush = true; + entry->mCacheFlush = (aRecord.GetClass() & kClassCacheFlushFlag); + entry->mTtl = aRecord.GetTtl(); } - - // Check for duplicates in the same response. - - entry = mNewEntries.FindMatching(data); - - if (entry == nullptr) + else { - entry = RecordDataEntry::Allocate(data); + entry = NewRecordEntry::Allocate(aRecord, data); OT_ASSERT(entry != nullptr); + mNewEntries.Push(*entry); } - - entry->mRecord.RefreshTtl(aRecord.GetTtl()); - -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 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. @@ -6649,14 +6702,19 @@ void Core::RecordCache::CommitNewResponseEntries(void) // `mCommittedEntries` that does not appear in the new list // and signal their removal. - if (mShouldFlush) + if (shouldFlush) { - for (RecordDataEntry &exitingEntry : mCommittedEntries) + for (RecordEntry &entry : mCommittedEntries) { - if (!mNewEntries.ContainsMatching(exitingEntry.mData)) + if (!entry.Matches(aRecordType)) { - exitingEntry.mRecord.RefreshTtl(0); - PrepareResultAndInvokeCallbacks(exitingEntry); + continue; + } + + if (!newMatchingEntries.ContainsMatching(entry.mType, entry.mData)) + { + entry.mRecord.RefreshTtl(0); + PrepareResultAndInvokeCallbacks(entry); } } } @@ -6664,18 +6722,18 @@ void Core::RecordCache::CommitNewResponseEntries(void) // Signal addition of any new entries or if there is any // change to an existing entry (TTL value changed). - for (const RecordDataEntry &newEntry : mNewEntries) + for (const NewRecordEntry &newEntry : newMatchingEntries) { - RecordDataEntry *exitingEntry = mCommittedEntries.FindMatching(newEntry.mData); - bool shouldSignal = false; + RecordEntry *entry = mCommittedEntries.FindMatching(newEntry.mType, newEntry.mData); + bool shouldSignal = false; - if (exitingEntry == nullptr) + if (entry == nullptr) { - shouldSignal = (newEntry.GetTtl() > 0); + shouldSignal = (newEntry.mTtl > 0); } else { - shouldSignal = (exitingEntry->GetTtl() != newEntry.GetTtl()); + shouldSignal = (entry->GetTtl() != newEntry.mTtl); } if (shouldSignal) @@ -6687,39 +6745,40 @@ void Core::RecordCache::CommitNewResponseEntries(void) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Now merge the new entries into the `mCommittedEntries` list. - if (mShouldFlush) + if (shouldFlush) { - mCommittedEntries.Clear(); - StopInitialQueries(); - mShouldFlush = false; + mCommittedEntries.RemoveAndFreeAllMatching(aRecordType); + + if (mRecordType != ResourceRecord::kTypeAny) + { + StopInitialQueries(); + } } - while (!mNewEntries.IsEmpty()) + while (!newMatchingEntries.IsEmpty()) { - OwnedPtr newEntry = mNewEntries.Pop(); - RecordDataEntry *entry; + OwnedPtr newEntry = newMatchingEntries.Pop(); + RecordEntry *entry; - entry = mCommittedEntries.FindMatching(newEntry->mData); + entry = mCommittedEntries.FindMatching(newEntry->mType, newEntry->mData); if (entry != nullptr) { - entry->mRecord.RefreshTtl(newEntry->GetTtl()); + entry->mRecord.RefreshTtl(newEntry->mTtl); } else { - mCommittedEntries.Push(*newEntry.Release()); + entry = RecordEntry::Allocate(*newEntry); + OT_ASSERT(entry != nullptr); + + mCommittedEntries.Push(*entry); } } - - mCommittedEntries.RemoveAndFreeAllMatching(EmptyChecker()); - - DetermineNextFireTime(); - ScheduleTimer(); } void Core::RecordCache::DetermineRecordFireTime(void) { - for (RecordDataEntry &entry : mCommittedEntries) + for (RecordEntry &entry : mCommittedEntries) { entry.mRecord.UpdateQueryAndFireTimeOn(*this); } @@ -6727,11 +6786,11 @@ void Core::RecordCache::DetermineRecordFireTime(void) void Core::RecordCache::ProcessExpiredRecords(TimeMilli aNow) { - OwningList expiredEntries; + OwningList expiredEntries; mCommittedEntries.RemoveAllMatching(expiredEntries, ExpireChecker(aNow)); - for (RecordDataEntry &entry : expiredEntries) + for (RecordEntry &entry : expiredEntries) { entry.mRecord.RefreshTtl(0); PrepareResultAndInvokeCallbacks(entry); @@ -6740,32 +6799,45 @@ void Core::RecordCache::ProcessExpiredRecords(TimeMilli aNow) void Core::RecordCache::ReportResultsTo(ResultCallback &aCallback) const { - for (const RecordDataEntry &entry : mCommittedEntries) + for (const RecordEntry &entry : mCommittedEntries) { RecordResult result; - PreareResultFor(entry, result); + PreareResultFor(entry.mType, entry.mData, entry.GetTtl(), result); aCallback.Invoke(GetInstance(), result); } } -void Core::RecordCache::PreareResultFor(const RecordDataEntry &aEntry, RecordResult &aResult) const +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 = mRecordType; - aResult.mRecordData = aEntry.mData.GetBytes(); - aResult.mRecordDataLength = aEntry.mData.GetLength(); - aResult.mTtl = aEntry.mRecord.GetTtl(); + aResult.mRecordType = aType; + aResult.mRecordData = aData.GetBytes(); + aResult.mRecordDataLength = aData.GetLength(); + aResult.mTtl = aTtl; aResult.mInfraIfIndex = Get().mInfraIfIndex; } -void Core::RecordCache::PrepareResultAndInvokeCallbacks(const RecordDataEntry &aEntry) +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(aEntry, result); + PreareResultFor(aType, aData, aTtl, result); InvokeCallbacks(result); } @@ -6785,20 +6857,48 @@ void Core::RecordCache::CopyInfoTo(RecordQuerier &aQuerier, CacheInfo &aInfo) co #endif //--------------------------------------------------------------------------------------------------------------------- -// Core::RecordCache::RecordDataEntry +// Core::RecordCache::NewRecordEntry -Core::RecordCache::RecordDataEntry::RecordDataEntry(Heap::Data &aData) +Core::RecordCache::NewRecordEntry::NewRecordEntry(const ResourceRecord &aRecord, Heap::Data &aData) : mNext(nullptr) + , mCacheFlush(aRecord.GetClass() & kClassCacheFlushFlag) + , mType(aRecord.GetType()) + , mTtl(aRecord.GetTtl()) , mData(static_cast(aData)) { } -bool Core::RecordCache::RecordDataEntry::Matches(const ExpireChecker &aExpireChecker) const +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(static_cast(aNewEntry.mData)) +{ + 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 ExpireChecker &aExpireChecker) const { return mRecord.ShouldExpire(aExpireChecker.mNow); } -bool Core::RecordCache::RecordDataEntry::Matches(EmptyChecker aChecker) const +bool Core::RecordCache::RecordEntry::Matches(EmptyChecker aChecker) const { OT_UNUSED_VARIABLE(aChecker); diff --git a/src/core/net/mdns.hpp b/src/core/net/mdns.hpp index cfd96e635..7c54290b0 100644 --- a/src/core/net/mdns.hpp +++ b/src/core/net/mdns.hpp @@ -1660,16 +1660,18 @@ private: template void InvokeCallbacks(const ResultType &aResult); private: - static constexpr uint32_t kMinIntervalBetweenQueries = 1000; // In msec - static constexpr uint32_t kNonActiveDeleteTimeout = 7 * Time::kOneMinuteInMsec; + static constexpr uint32_t kMinIntervalBetweenQueries = 1000; // In msec + static constexpr uint32_t kNonActiveDeleteTimeout = 7 * Time::kOneMinuteInMsec; + static constexpr uint32_t kNonActiveDeleteTimeoutForAnyRecord = 1 * Time::kOneSecondInMsec; typedef OwningList CallbackList; - void SetIsActive(bool aIsActive); - bool ShouldQuery(TimeMilli aNow); - void PrepareQuery(CacheContext &aContext); - void ProcessExpiredRecords(TimeMilli aNow); - void DetermineNextInitialQueryTime(void); + void SetIsActive(bool aIsActive); + uint32_t DetermineDeleteTimeout(void) const; + bool ShouldQuery(TimeMilli aNow); + void PrepareQuery(CacheContext &aContext); + void ProcessExpiredRecords(TimeMilli aNow); + void DetermineNextInitialQueryTime(void); ResultCallback *FindCallbackMatching(const ResultCallback &aCallback); @@ -1974,17 +1976,34 @@ private: #endif private: - struct RecordDataEntry : public LinkedListEntry, public Heap::Allocatable + struct NewRecordEntry : public LinkedListEntry, public Heap::Allocatable { - explicit RecordDataEntry(Heap::Data &aData); - bool Matches(const Heap::Data &aData) const { return (mData == aData); } + NewRecordEntry(const ResourceRecord &aRecord, Heap::Data &aData); + + bool Matches(uint16_t aType) const; + bool Matches(uint16_t aType, const Heap::Data &aData) const; + + NewRecordEntry *mNext; + bool mCacheFlush; + uint16_t mType; + uint32_t mTtl; + Heap::Data mData; + }; + + struct RecordEntry : public LinkedListEntry, public Heap::Allocatable + { + explicit RecordEntry(NewRecordEntry &aNewEntry); + + bool Matches(uint16_t aType) const; + bool Matches(uint16_t aType, const Heap::Data &aData) const; bool Matches(const ExpireChecker &aExpireChecker) const; bool Matches(EmptyChecker aChecker) const; uint32_t GetTtl(void) const { return mRecord.GetTtl(); } - RecordDataEntry *mNext; - Heap::Data mData; - CacheRecordInfo mRecord; + RecordEntry *mNext; + uint16_t mType; + Heap::Data mData; + CacheRecordInfo mRecord; }; // Called by base class `CacheEntry` @@ -1995,17 +2014,19 @@ private: void ReportResultsTo(ResultCallback &aCallback) const; Error Init(Instance &aInstance, const RecordQuerier &aQuerier); + void CommitNewEntriesForType(uint16_t aRecordType); void AppendNameTo(TxMessage &aTxMessage, Section aSection); - void PreareResultFor(const RecordDataEntry &aEntry, RecordResult &aResult) const; - void PrepareResultAndInvokeCallbacks(const RecordDataEntry &aEntry); + void PreareResultFor(uint16_t aType, const Heap::Data &aData, uint32_t aTtl, RecordResult &aResult) const; + void PrepareResultAndInvokeCallbacks(const NewRecordEntry &aNewEntry); + void PrepareResultAndInvokeCallbacks(const RecordEntry &aEntry); + void PrepareResultAndInvokeCallbacks(uint16_t aType, const Heap::Data &aData, uint32_t aTtl); - RecordCache *mNext; - Heap::String mFirstLabel; - Heap::String mNextLabels; - uint16_t mRecordType; - OwningList mCommittedEntries; - OwningList mNewEntries; - bool mShouldFlush; + RecordCache *mNext; + Heap::String mFirstLabel; + Heap::String mNextLabels; + uint16_t mRecordType; + OwningList mNewEntries; + OwningList mCommittedEntries; }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/unit/test_mdns.cpp b/tests/unit/test_mdns.cpp index bb3edf809..70702cf78 100644 --- a/tests/unit/test_mdns.cpp +++ b/tests/unit/test_mdns.cpp @@ -1376,16 +1376,14 @@ static void SendHostAddrResponse(const char *aHostName, struct RecordData { + uint16_t mType; const uint8_t *mData; uint16_t mLength; uint32_t mTtl; + bool mCacheFlush; }; -static void SendRecordResponse(const char *aName, - uint16_t aRecordType, - bool aCacheFlush, - uint16_t aNumRecords, - const RecordData *aRecords) +static void SendRecordResponse(const char *aName, uint16_t aNumRecords, const RecordData *aRecords) { Message *message; Header header; @@ -1401,29 +1399,33 @@ static void SendRecordResponse(const char *aName, SuccessOrQuit(message->Append(header)); + Log("Sending response with %u records", aNumRecords); + for (uint16_t index = 0; index < aNumRecords; index++) { + const RecordData &record = aRecords[index]; + SuccessOrQuit(Name::AppendName(aName, *message)); - rr.Init(aRecordType); + rr.Init(record.mType); - if (aCacheFlush) + if (record.mCacheFlush) { rr.SetClass(rr.GetClass() | kClassCacheFlushFlag); } - rr.SetTtl(aRecords[index].mTtl); - rr.SetLength(aRecords[index].mLength); + rr.SetTtl(record.mTtl); + rr.SetLength(record.mLength); SuccessOrQuit(message->Append(rr)); - SuccessOrQuit(message->AppendBytes(aRecords[index].mData, aRecords[index].mLength)); + SuccessOrQuit(message->AppendBytes(record.mData, record.mLength)); + + Log(" Record %u (cache-flush:%u) for %s", record.mType, record.mCacheFlush, aName); } SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address)); senderAddrInfo.mPort = kMdnsPort; senderAddrInfo.mInfraIfIndex = 0; - Log("Sending record %u response for %s, num-records %u", aRecordType, aName, aNumRecords); - otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo); } @@ -6929,12 +6931,14 @@ void TestRecordQuerier(void) Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); Log("Send a response. Validate callback result."); - records[0].mData = kKey1; - records[0].mLength = sizeof(kKey1); - records[0].mTtl = 120; + records[0].mType = ResourceRecord::kTypeKey; + records[0].mData = kKey1; + records[0].mLength = sizeof(kKey1); + records[0].mTtl = 120; + records[0].mCacheFlush = false; sRecordCallbacks.Clear(); - SendRecordResponse("mysrv._srv._udp.local.", ResourceRecord::kTypeKey, /* aCacheFlush */ false, 1, records); + SendRecordResponse("mysrv._srv._udp.local.", 1, records); AdvanceTime(1); @@ -6952,12 +6956,14 @@ void TestRecordQuerier(void) Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); Log("Send a second response (without cache-flush). Validate callback result."); - records[0].mData = kKey2; - records[0].mLength = sizeof(kKey2); - records[0].mTtl = 120; + records[0].mType = ResourceRecord::kTypeKey; + records[0].mData = kKey2; + records[0].mLength = sizeof(kKey2); + records[0].mTtl = 120; + records[0].mCacheFlush = false; sRecordCallbacks.Clear(); - SendRecordResponse("mysrv._srv._udp.local.", ResourceRecord::kTypeKey, /* aCacheFlush */ false, 1, records); + SendRecordResponse("mysrv._srv._udp.local.", 1, records); AdvanceTime(1); @@ -7017,16 +7023,20 @@ void TestRecordQuerier(void) Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); Log("Send a response (without cache-flush) with one previous record and a new record."); - records[0].mData = kKey1; - records[0].mLength = sizeof(kKey1); - records[0].mTtl = 120; + records[0].mType = ResourceRecord::kTypeKey; + records[0].mData = kKey1; + records[0].mLength = sizeof(kKey1); + records[0].mTtl = 120; + records[0].mCacheFlush = false; - records[1].mData = kKey3; - records[1].mLength = sizeof(kKey3); - records[1].mTtl = 120; + records[1].mType = ResourceRecord::kTypeKey; + records[1].mData = kKey3; + records[1].mLength = sizeof(kKey3); + records[1].mTtl = 120; + records[1].mCacheFlush = false; sRecordCallbacks.Clear(); - SendRecordResponse("mysrv._srv._udp.local.", ResourceRecord::kTypeKey, /* aCacheFlush */ false, 2, records); + SendRecordResponse("mysrv._srv._udp.local.", 2, records); AdvanceTime(1); @@ -7046,14 +7056,27 @@ void TestRecordQuerier(void) AdvanceTime(5000); Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); - Log("Send a response (with cache-flush) with only one record, `key3`."); + Log("Send a response with one record, `key3` (cached-flush) and an extra record of different type"); - records[0].mData = kKey3; - records[0].mLength = sizeof(kKey3); - records[0].mTtl = 120; + records[0].mType = ResourceRecord::kTypeKey; + records[0].mData = kKey3; + records[0].mLength = sizeof(kKey3); + records[0].mTtl = 120; + records[0].mCacheFlush = true; + + // The second record is of type TXT, which should be ignored + // because it doesn't match the `RecordQuerier`. We intentionally + // use `kKey2` as record data to validate that the record type + // (TXT) and not just is indeed checked by the mDNS module. + + records[1].mType = ResourceRecord::kTypeTxt; + records[1].mData = kKey2; + records[1].mLength = sizeof(kKey2); + records[1].mTtl = 120; + records[1].mCacheFlush = true; sRecordCallbacks.Clear(); - SendRecordResponse("mysrv._srv._udp.local.", ResourceRecord::kTypeKey, /* aCacheFlush */ true, 1, records); + SendRecordResponse("mysrv._srv._udp.local.", 2, records); AdvanceTime(1); @@ -7082,20 +7105,26 @@ void TestRecordQuerier(void) Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); Log("Send a response removing key3 and other keys."); - records[0].mData = kKey1; - records[0].mLength = sizeof(kKey1); - records[0].mTtl = 0; + records[0].mType = ResourceRecord::kTypeKey; + records[0].mData = kKey1; + records[0].mLength = sizeof(kKey1); + records[0].mTtl = 0; + records[0].mCacheFlush = false; - records[1].mData = kKey2; - records[1].mLength = sizeof(kKey2); - records[1].mTtl = 0; + records[1].mType = ResourceRecord::kTypeKey; + records[1].mData = kKey2; + records[1].mLength = sizeof(kKey2); + records[1].mTtl = 0; + records[1].mCacheFlush = false; - records[2].mData = kKey3; - records[2].mLength = sizeof(kKey3); - records[2].mTtl = 0; + records[2].mType = ResourceRecord::kTypeKey; + records[2].mData = kKey3; + records[2].mLength = sizeof(kKey3); + records[2].mTtl = 0; + records[2].mCacheFlush = false; sRecordCallbacks.Clear(); - SendRecordResponse("mysrv._srv._udp.local.", ResourceRecord::kTypeKey, /* aCacheFlush */ false, 3, records); + SendRecordResponse("mysrv._srv._udp.local.", 3, records); AdvanceTime(1); @@ -7116,16 +7145,20 @@ void TestRecordQuerier(void) Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); Log("Send a response adding two keys"); - records[0].mData = kKey1; - records[0].mLength = sizeof(kKey1); - records[0].mTtl = 500; + records[0].mType = ResourceRecord::kTypeKey; + records[0].mData = kKey1; + records[0].mLength = sizeof(kKey1); + records[0].mTtl = 500; + records[0].mCacheFlush = true; - records[1].mData = kKey2; - records[1].mLength = sizeof(kKey2); - records[1].mTtl = 500; + records[1].mType = ResourceRecord::kTypeKey; + records[1].mData = kKey2; + records[1].mLength = sizeof(kKey2); + records[1].mTtl = 500; + records[1].mCacheFlush = true; sRecordCallbacks.Clear(); - SendRecordResponse("mysrv._srv._udp.local.", ResourceRecord::kTypeKey, /* aCacheFlush */ true, 2, records); + SendRecordResponse("mysrv._srv._udp.local.", 2, records); AdvanceTime(1); @@ -7154,12 +7187,14 @@ void TestRecordQuerier(void) Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); Log("Send a response changing the TTL for key1"); - records[0].mData = kKey1; - records[0].mLength = sizeof(kKey1); - records[0].mTtl = 120; + records[0].mType = ResourceRecord::kTypeKey; + records[0].mData = kKey1; + records[0].mLength = sizeof(kKey1); + records[0].mTtl = 120; + records[0].mCacheFlush = false; sRecordCallbacks.Clear(); - SendRecordResponse("mysrv._srv._udp.local.", ResourceRecord::kTypeKey, /* aCacheFlush */ false, 1, records); + SendRecordResponse("mysrv._srv._udp.local.", 1, records); AdvanceTime(1); @@ -7203,7 +7238,7 @@ void TestRecordQuerier(void) AdvanceTime(10); - SendRecordResponse("mysrv._srv._udp.local.", ResourceRecord::kTypeKey, /* aCacheFlush */ false, 1, records); + SendRecordResponse("mysrv._srv._udp.local.", 1, records); Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); Log("Check queries are sent at 80, 85, 90, 95 percentages of TTL."); @@ -7328,6 +7363,301 @@ void TestRecordQuerier(void) testFreeInstance(sInstance); } +void TestRecordQuerierForAny(void) +{ + static constexpr uint8_t kMaxResponseRecords = 6; + + Core *mdns = InitTest(); + Core::RecordQuerier querier; + Core::RecordQuerier querier2; + Core::Iterator *iterator; + Core::CacheInfo cacheInfo; + const DnsMessage *dnsMsg; + const RecordCallback *recordCallback; + uint16_t heapAllocations; + RecordData records[kMaxResponseRecords]; + + Log("-------------------------------------------------------------------------------------------"); + Log("TestRecordQuerierForAny"); + + AdvanceTime(1); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex)); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Start a record querier for ANY record type. Validate initial queries."); + + ClearAllBytes(querier); + + querier.mFirstLabel = "mysrv"; + querier.mNextLabels = "_srv._udp"; + querier.mRecordType = ResourceRecord::kTypeAny; + querier.mInfraIfIndex = kInfraIfIndex; + querier.mCallback = HandleRecordResult; + + sDnsMessages.Clear(); + SuccessOrQuit(mdns->StartRecordQuerier(querier)); + + for (uint8_t queryCount = 0; queryCount < kNumInitalQueries; queryCount++) + { + sDnsMessages.Clear(); + + AdvanceTime((queryCount == 0) ? 125 : (1U << (queryCount - 1)) * 1000); + + VerifyOrQuit(!sDnsMessages.IsEmpty()); + dnsMsg = sDnsMessages.GetHead(); + dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0); + dnsMsg->ValidateAsQueryFor(querier); + VerifyOrQuit(dnsMsg->GetNext() == nullptr); + } + + sDnsMessages.Clear(); + + AdvanceTime(20 * 1000); + VerifyOrQuit(sDnsMessages.IsEmpty()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Send a response. Validate callback result."); + + records[0].mType = ResourceRecord::kTypeKey; + records[0].mData = kKey1; + records[0].mLength = sizeof(kKey1); + records[0].mTtl = 120; + records[0].mCacheFlush = false; + + sRecordCallbacks.Clear(); + SendRecordResponse("mysrv._srv._udp.local.", 1, records); + + AdvanceTime(1); + + VerifyOrQuit(!sRecordCallbacks.IsEmpty()); + recordCallback = sRecordCallbacks.GetHead(); + VerifyOrQuit(recordCallback->mFirstLabel.Matches("mysrv")); + VerifyOrQuit(recordCallback->mNextLabels.Matches("_srv._udp")); + VerifyOrQuit(recordCallback->mRecordType == ResourceRecord::kTypeKey); + VerifyOrQuit(recordCallback->MatchesData(kKey1)); + VerifyOrQuit(recordCallback->mTtl == 120); + VerifyOrQuit(recordCallback->GetNext() == nullptr); + + VerifyOrQuit(sDnsMessages.IsEmpty()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Send a second response for a different record type. Validate callback result."); + + records[0].mType = ResourceRecord::kTypeTxt; + records[0].mData = kKey2; + records[0].mLength = sizeof(kKey2); + records[0].mTtl = 400; + records[0].mCacheFlush = true; + + sRecordCallbacks.Clear(); + SendRecordResponse("mysrv._srv._udp.local.", 1, records); + + AdvanceTime(1); + + VerifyOrQuit(!sRecordCallbacks.IsEmpty()); + recordCallback = sRecordCallbacks.GetHead(); + VerifyOrQuit(recordCallback->mFirstLabel.Matches("mysrv")); + VerifyOrQuit(recordCallback->mNextLabels.Matches("_srv._udp")); + VerifyOrQuit(recordCallback->mRecordType == ResourceRecord::kTypeTxt); + VerifyOrQuit(recordCallback->MatchesData(kKey2)); + VerifyOrQuit(recordCallback->mTtl == 400); + VerifyOrQuit(recordCallback->GetNext() == nullptr); + + VerifyOrQuit(sDnsMessages.IsEmpty()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Send a response with multiple record of different types"); + + records[0].mType = ResourceRecord::kTypeKey; + records[0].mData = kKey1; + records[0].mLength = sizeof(kKey1); + records[0].mTtl = 200; + records[0].mCacheFlush = false; + + records[1].mType = ResourceRecord::kTypeTxt; + records[1].mData = kKey2; + records[1].mLength = sizeof(kKey2); + records[1].mTtl = 300; + records[1].mCacheFlush = true; + + records[2].mType = ResourceRecord::kTypeKey; + records[2].mData = kKey2; + records[2].mLength = sizeof(kKey2); + records[2].mTtl = 200; + records[2].mCacheFlush = false; + + records[3].mType = ResourceRecord::kTypeKey; + records[3].mData = kKey3; + records[3].mLength = sizeof(kKey3); + records[3].mTtl = 200; + records[3].mCacheFlush = false; + + sRecordCallbacks.Clear(); + SendRecordResponse("mysrv._srv._udp.local.", 4, records); + + AdvanceTime(1); + + VerifyOrQuit(!sRecordCallbacks.IsEmpty()); + recordCallback = sRecordCallbacks.GetHead(); + + for (uint8_t num = 4; num > 0; num--) + { + VerifyOrQuit(recordCallback != nullptr); + VerifyOrQuit(recordCallback->mFirstLabel.Matches("mysrv")); + VerifyOrQuit(recordCallback->mNextLabels.Matches("_srv._udp")); + + if (recordCallback->mRecordType == ResourceRecord::kTypeTxt) + { + VerifyOrQuit(recordCallback->MatchesData(kKey2)); + VerifyOrQuit(recordCallback->mTtl == 300); + } + else if (recordCallback->mRecordType == ResourceRecord::kTypeKey) + { + VerifyOrQuit(recordCallback->mTtl == 200); + VerifyOrQuit(recordCallback->MatchesData(kKey1) || recordCallback->MatchesData(kKey2) || + recordCallback->MatchesData(kKey3)); + } + else + { + VerifyOrQuit(false); + } + + recordCallback = recordCallback->GetNext(); + } + + VerifyOrQuit(recordCallback == nullptr); + VerifyOrQuit(sDnsMessages.IsEmpty()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Start another record querier for the same name with different callback."); + + ClearAllBytes(querier2); + + querier2.mFirstLabel = "mysrv"; + querier2.mNextLabels = "_srv._udp"; + querier2.mRecordType = ResourceRecord::kTypeAny; + querier2.mInfraIfIndex = kInfraIfIndex; + querier2.mCallback = HandleRecordResultAlternate; + + sRecordCallbacks.Clear(); + SuccessOrQuit(mdns->StartRecordQuerier(querier2)); + + AdvanceTime(1); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Validate callback result from cache for the new querier"); + + VerifyOrQuit(!sRecordCallbacks.IsEmpty()); + recordCallback = sRecordCallbacks.GetHead(); + + for (uint8_t num = 4; num > 0; num--) + { + VerifyOrQuit(recordCallback != nullptr); + VerifyOrQuit(recordCallback->mFirstLabel.Matches("mysrv")); + VerifyOrQuit(recordCallback->mNextLabels.Matches("_srv._udp")); + + if (recordCallback->mRecordType == ResourceRecord::kTypeTxt) + { + VerifyOrQuit(recordCallback->MatchesData(kKey2)); + VerifyOrQuit(recordCallback->mTtl == 300); + } + else if (recordCallback->mRecordType == ResourceRecord::kTypeKey) + { + VerifyOrQuit(recordCallback->mTtl == 200); + VerifyOrQuit(recordCallback->MatchesData(kKey1) || recordCallback->MatchesData(kKey2) || + recordCallback->MatchesData(kKey3)); + } + else + { + VerifyOrQuit(false); + } + + recordCallback = recordCallback->GetNext(); + } + + VerifyOrQuit(recordCallback == nullptr); + + VerifyOrQuit(sDnsMessages.IsEmpty()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Stop the second querier."); + + SuccessOrQuit(mdns->StopRecordQuerier(querier2)); + +#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Check the list of `RecordQueier` entries and the cache-info"); + + iterator = mdns->AllocateIterator(); + VerifyOrQuit(iterator != nullptr); + + SuccessOrQuit(mdns->GetNextRecordQuerier(*iterator, querier2, cacheInfo)); + VerifyOrQuit(querier2.mRecordType == ResourceRecord::kTypeAny); + VerifyOrQuit(StringMatch(querier2.mFirstLabel, "mysrv", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringMatch(querier2.mNextLabels, "_srv._udp", kStringCaseInsensitiveMatch)); + + VerifyOrQuit(cacheInfo.mIsActive); + VerifyOrQuit(cacheInfo.mHasCachedResults); + + VerifyOrQuit(mdns->GetNextRecordQuerier(*iterator, querier2, cacheInfo) == kErrorNotFound); + + mdns->FreeIterator(*iterator); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Stop the record querier"); + + SuccessOrQuit(mdns->StopRecordQuerier(querier)); + + sDnsMessages.Clear(); + + AdvanceTime(10); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Check the list of `RecordQueier` entries and cache-info after stop (no longer active)"); + + iterator = mdns->AllocateIterator(); + VerifyOrQuit(iterator != nullptr); + + SuccessOrQuit(mdns->GetNextRecordQuerier(*iterator, querier2, cacheInfo)); + VerifyOrQuit(querier2.mRecordType == ResourceRecord::kTypeAny); + VerifyOrQuit(StringMatch(querier2.mFirstLabel, "mysrv", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringMatch(querier2.mNextLabels, "_srv._udp", kStringCaseInsensitiveMatch)); + + VerifyOrQuit(!cacheInfo.mIsActive); + VerifyOrQuit(cacheInfo.mHasCachedResults); + + VerifyOrQuit(mdns->GetNextRecordQuerier(*iterator, querier2, cacheInfo) == kErrorNotFound); + + mdns->FreeIterator(*iterator); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Check the `RecordQuerier` is correctly removed after 'remove timeout' of 1 minutes"); + + AdvanceTime(1 * 60 * 1000); + VerifyOrQuit(sDnsMessages.IsEmpty()); + + iterator = mdns->AllocateIterator(); + VerifyOrQuit(iterator != nullptr); + + VerifyOrQuit(mdns->GetNextRecordQuerier(*iterator, querier2, cacheInfo) == kErrorNotFound); + + mdns->FreeIterator(*iterator); + +#endif // OPENTHREAD_CONFIG_MULTICAST_DNS_ENTRY_ITERATION_API_ENABLE + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + + SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex)); + VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations); + + Log("End of test"); + + testFreeInstance(sInstance); +} + void TestPassiveCache(void) { static const char *const kSubTypes[] = {"_sub1", "_xyzw"}; @@ -7870,6 +8200,7 @@ int main(void) ot::Dns::Multicast::TestTxtResolver(); ot::Dns::Multicast::TestIp6AddrResolver(); ot::Dns::Multicast::TestRecordQuerier(); + ot::Dns::Multicast::TestRecordQuerierForAny(); ot::Dns::Multicast::TestPassiveCache(); ot::Dns::Multicast::TestLegacyUnicastResponse();