[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.
This commit is contained in:
Jonathan Hui
2026-10-05 18:03:52 -07:00
committed by GitHub
parent 9c22c346c5
commit dce84565ea
3 changed files with 105 additions and 2 deletions
+46 -2
View File
@@ -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
+5
View File
@@ -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;
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+54
View File
@@ -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");