diff --git a/src/modules/TrafficManagementModule.cpp b/src/modules/TrafficManagementModule.cpp index ea5054ded..30a298019 100644 --- a/src/modules/TrafficManagementModule.cpp +++ b/src/modules/TrafficManagementModule.cpp @@ -1092,6 +1092,12 @@ bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacke if (!sendResponse) return true; + // Checked here, once a reply would actually go out, so declined requests do not consume the budget. + if (!directResponseAllowed(getFrom(p), clockMs())) { + TM_LOG_DEBUG("NodeInfo direct response throttled for 0x%08x", getFrom(p)); + return false; + } + meshtastic_MeshPacket *reply = router->allocForSending(); if (!reply) { TM_LOG_WARN("NodeInfo direct response dropped: no packet buffer"); @@ -1144,6 +1150,43 @@ bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacke return true; } +bool TrafficManagementModule::directResponseAllowed(NodeNum requestor, uint32_t nowMs) +{ + // Reached from the packet path and from runOnce, so the throttle state needs the same lock as the cache. + concurrency::LockGuard guard(&cacheLock); + + if (lastDirectResponseMs != 0 && (nowMs - lastDirectResponseMs) < kDirectResponseGlobalMs) + return false; + + DirectResponseThrottleEntry *slot = nullptr; + for (size_t i = 0; i < kDirectResponseTrackedRequestors; i++) { + if (directResponseSeen[i].requestor == requestor) { + if ((nowMs - directResponseSeen[i].lastReplyMs) < kDirectResponsePerRequestorMs) + return false; + slot = &directResponseSeen[i]; + break; + } + } + + if (slot == nullptr) { + // Unseen requester: take a free slot, otherwise reuse the least recently used one. + slot = &directResponseSeen[0]; + for (size_t i = 0; i < kDirectResponseTrackedRequestors; i++) { + if (directResponseSeen[i].requestor == 0) { + slot = &directResponseSeen[i]; + break; + } + if (directResponseSeen[i].lastReplyMs < slot->lastReplyMs) + slot = &directResponseSeen[i]; + } + } + + slot->requestor = requestor; + slot->lastReplyMs = nowMs; + lastDirectResponseMs = nowMs; + return true; +} + bool TrafficManagementModule::isMinHopsFromRequestor(const meshtastic_MeshPacket *p) const { int8_t hopsAway = getHopsAway(*p, -1); diff --git a/src/modules/TrafficManagementModule.h b/src/modules/TrafficManagementModule.h index f4a1d5954..a3619fc0a 100644 --- a/src/modules/TrafficManagementModule.h +++ b/src/modules/TrafficManagementModule.h @@ -293,6 +293,19 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread bool shouldDropPosition(const meshtastic_MeshPacket *p, const meshtastic_Position *pos, uint32_t nowMs); bool shouldRespondToNodeInfo(const meshtastic_MeshPacket *p, bool sendResponse); + + // Replies go to the requesting packet's unauthenticated `from`, so space them per requester to bound + // what any one node can be made to receive, plus a global floor on airtime. + static constexpr uint32_t kDirectResponsePerRequestorMs = 60'000UL; + static constexpr uint32_t kDirectResponseGlobalMs = 1'000UL; + static constexpr size_t kDirectResponseTrackedRequestors = 8; + struct DirectResponseThrottleEntry { + NodeNum requestor; + uint32_t lastReplyMs; + }; + DirectResponseThrottleEntry directResponseSeen[kDirectResponseTrackedRequestors] = {}; + uint32_t lastDirectResponseMs = 0; + bool directResponseAllowed(NodeNum requestor, uint32_t nowMs); bool isMinHopsFromRequestor(const meshtastic_MeshPacket *p) const; bool isRateLimited(NodeNum from, uint32_t nowMs); bool shouldDropUnknown(const meshtastic_MeshPacket *p, uint32_t nowMs);