From dce84565ea49a8bf52eee8518d3005e8e0111705 Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Mon, 5 Oct 2026 18:03:52 -0700 Subject: [PATCH] [mdns] rate-limit responses to repeated QU queries (#13675) Currently, `RecordInfo::ScheduleAnswer()` enforces a 1-second minimum interval (`kMinIntervalBetweenMulticast`) between multicast responses for the same record, but unconditionally sets `mUnicastAnswerPending = true` for every query with the `QU` (unicast-response) bit set. If a host on the LAN repeatedly sends `QU` queries for a registered name or service, the mDNS responder generates and transmits a unicast response packet for every incoming query without rate limiting, causing excessive packet and CPU load. This commit adds per-record rate limiting for responses to non-probe `QU` queries in `RecordInfo`: - Track `mLastUnicastTime` and `mIsLastUnicastValid` in `RecordInfo` with the same aging mechanism (`kLastUnicastTimeAge`) used for `mLastMulticastTime`. - In `RecordInfo::ScheduleAnswer()`, enforce a 1-second minimum interval (`kMinIntervalBetweenUnicast = 1000` ms) between unicast responses for the same record when answering non-probe `QU` queries. If the unicast rate limit is reached, fall through to schedule a multicast response instead (which is subject to multicast rate limiting) so other queriers on the network can still receive the answer. - Update `TestQuery()` in `test_mdns.cpp` to verify `QU` rate limiting and multicast fallback behavior. --- src/core/net/mdns.cpp | 48 +++++++++++++++++++++++++++++++++-- src/core/net/mdns.hpp | 5 ++++ tests/unit/test_mdns.cpp | 54 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/src/core/net/mdns.cpp b/src/core/net/mdns.cpp index 5b9f99d57..d2a3002b8 100644 --- a/src/core/net/mdns.cpp +++ b/src/core/net/mdns.cpp @@ -851,8 +851,19 @@ void Core::RecordInfo::ScheduleAnswer(const AnswerInfo &aInfo) if (aInfo.mUnicastResponse || aInfo.mLegacyUnicastResponse) { - mUnicastAnswerPending = true; - ExitNow(); + VerifyOrExit(!mUnicastAnswerPending); + + if (aInfo.mIsProbe || aInfo.mLegacyUnicastResponse || + (GetDurationSinceLastUnicast(aInfo.mQueryRxTime) >= kMinIntervalBetweenUnicast)) + { + mUnicastAnswerPending = true; + ExitNow(); + } + + // If the unicast rate limit (`kMinIntervalBetweenUnicast`) is + // reached on a `QU` query, fall through to schedule a multicast + // response instead (which has its own rate-limiting logic) so + // other queriers on the network can still receive the answer. } if (!aInfo.mIsProbe) @@ -1052,6 +1063,20 @@ void Core::RecordInfo::UpdateFireTimeOn(FireTime &aFireTime) } } + if (mIsLastUnicastValid) + { + TimeMilli lastUnicastAgeTime = mLastUnicastTime + kLastUnicastTimeAge; + + if (lastUnicastAgeTime <= TimerMilli::GetNow()) + { + mIsLastUnicastValid = false; + } + else + { + aFireTime.SetFireTime(lastUnicastAgeTime); + } + } + exit: return; } @@ -1094,6 +1119,11 @@ void Core::RecordInfo::MarkAsAppended(TxMessage &aTxMessage, Section aSection) break; case TxMessage::kUnicastResponse: + mAppendState = kAppendedInUnicastMsg; + mLastUnicastTime = TimerMilli::GetNow(); + mIsLastUnicastValid = true; + break; + case TxMessage::kLegacyUnicastResponse: mAppendState = kAppendedInUnicastMsg; break; @@ -1156,6 +1186,20 @@ exit: return duration; } +uint32_t Core::RecordInfo::GetDurationSinceLastUnicast(TimeMilli aTime) const +{ + uint32_t duration; + + SetToUintMax(duration); + + VerifyOrExit(mIsPresent && mIsLastUnicastValid); + VerifyOrExit(aTime > mLastUnicastTime, duration = 0); + duration = aTime - mLastUnicastTime; + +exit: + return duration; +} + //---------------------------------------------------------------------------------------------------------------------- // Core::FireTime diff --git a/src/core/net/mdns.hpp b/src/core/net/mdns.hpp index 211bd2e76..fd7e08906 100644 --- a/src/core/net/mdns.hpp +++ b/src/core/net/mdns.hpp @@ -1091,6 +1091,7 @@ private: void UpdateFireTimeOn(FireTime &aFireTime); void DetermineNextAggrTxTime(NextFireTime &aNextAggrTxTime) const; uint32_t GetDurationSinceLastMulticast(TimeMilli aTime) const; + uint32_t GetDurationSinceLastUnicast(TimeMilli aTime) const; Error GetLastMulticastTime(TimeMilli &aLastMulticastTime) const; // `AppendState` methods: Used to track whether the record @@ -1116,7 +1117,9 @@ private: TimeMilli GetAnswerTime(void) const { return mQueryRxTime + mAnswerDelay; } static constexpr uint32_t kMinIntervalBetweenMulticast = 1000; // msec + static constexpr uint32_t kMinIntervalBetweenUnicast = 1000; // msec static constexpr uint32_t kLastMulticastTimeAge = 10 * Time::kOneHourInMsec; + static constexpr uint32_t kLastUnicastTimeAge = 10 * Time::kOneHourInMsec; static_assert(kNotAppended == 0, "kNotAppended MUST be zero, so `Clear()` works correctly"); @@ -1124,6 +1127,7 @@ private: bool mMulticastAnswerPending : 1; bool mUnicastAnswerPending : 1; bool mIsLastMulticastValid : 1; + bool mIsLastUnicastValid : 1; bool mCanExtendAnswerDelay : 1; uint8_t mAnnounceCounter; AppendState mAppendState; @@ -1133,6 +1137,7 @@ private: TimeMilli mAnnounceTime; TimeMilli mQueryRxTime; TimeMilli mLastMulticastTime; + TimeMilli mLastUnicastTime; }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/unit/test_mdns.cpp b/tests/unit/test_mdns.cpp index 65676bc5f..15c28ba30 100644 --- a/tests/unit/test_mdns.cpp +++ b/tests/unit/test_mdns.cpp @@ -4368,6 +4368,15 @@ void TestQuery(void) dnsMsg->Validate(host1, kInAdditionalSection); dnsMsg->Validate(host2, kInAdditionalSection); + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Resend the same unicast-response (QU) query and validate that response is not emitted (rate limit)"); + + sDnsMessages.Clear(); + SendQuery("_srv._udp.local.", ResourceRecord::kTypePtr, ResourceRecord::kClassInternet | kClassQueryUnicastFlag); + + AdvanceTime(200); + VerifyOrQuit(sDnsMessages.IsEmpty()); + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); Log("Resend the same multicast query and validate that response is not emitted (rate limit)"); @@ -4397,6 +4406,51 @@ void TestQuery(void) dnsMsg->Validate(host1, kInAdditionalSection); dnsMsg->Validate(host2, kInAdditionalSection); + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Resend the unicast-response (QU) query after > 1 second and validate the response"); + + sDnsMessages.Clear(); + SendQuery("_srv._udp.local.", ResourceRecord::kTypePtr, ResourceRecord::kClassInternet | kClassQueryUnicastFlag); + + AdvanceTime(200); + + dnsMsg = sDnsMessages.GetHead(); + VerifyOrQuit(dnsMsg != nullptr); + VerifyOrQuit(dnsMsg->GetNext() == nullptr); + + dnsMsg->ValidateHeader(kUnicastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 9); + + dnsMsg->Validate(service1, kInAnswerSection, kCheckPtr); + dnsMsg->Validate(service3, kInAnswerSection, kCheckPtr); + dnsMsg->Validate(service1, kInAdditionalSection, kCheckSrv | kCheckTxt); + dnsMsg->Validate(service3, kInAdditionalSection, kCheckSrv | kCheckTxt); + dnsMsg->Validate(host1, kInAdditionalSection); + dnsMsg->Validate(host2, kInAdditionalSection); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Resend the QU query before 1s unicast interval (when > 1s since last multicast) and validate multicast " + "response"); + + AdvanceTime(750); + + sDnsMessages.Clear(); + SendQuery("_srv._udp.local.", ResourceRecord::kTypePtr, ResourceRecord::kClassInternet | kClassQueryUnicastFlag); + + AdvanceTime(200); + + dnsMsg = sDnsMessages.GetHead(); + VerifyOrQuit(dnsMsg != nullptr); + VerifyOrQuit(dnsMsg->GetNext() == nullptr); + + dnsMsg->ValidateHeader(kMulticastResponse, /* Q */ 0, /* Ans */ 2, /* Auth */ 0, /* Addnl */ 9); + + dnsMsg->Validate(service1, kInAnswerSection, kCheckPtr); + dnsMsg->Validate(service3, kInAnswerSection, kCheckPtr); + dnsMsg->Validate(service1, kInAdditionalSection, kCheckSrv | kCheckTxt); + dnsMsg->Validate(service3, kInAdditionalSection, kCheckSrv | kCheckTxt); + dnsMsg->Validate(host1, kInAdditionalSection); + dnsMsg->Validate(host2, kInAdditionalSection); + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); Log("Browse for sub-type `_s._sub._srv._udp` and validate two answers");