Refuse broadcast NodeInfo requests, and back off on a rolling NodeDB - #11423
Draft
jp-bennett wants to merge 2 commits into
Draft
jp-bennett wants to merge 2 commits into
jp-bennett wants to merge 2 commits into
Conversation
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Contributor
⚡ Try this PR in the Web FlasherNote Building this pull request… the flash button, badges and supported-board |
…a rolling NodeDB A `want_response` NodeInfo addressed to NODENUM_BROADCAST asks every node in earshot to answer one packet. That is amplification, and it is how a NodeInfo storm starts: the burst of replies is itself traffic from nodes the listeners have never seen, so each of those gets greeted in turn, and on a mesh larger than MAX_NUM_NODES the greetings evict the peers that prompted them. allocReply() now refuses any NodeInfo request sent to the broadcast address. Unicast requests are still answered, so the targeted exchanges are unaffected: the unknown-node greeting, ReliableRouter's PKI_UNKNOWN_PUBKEY response to a DM we could not decrypt, and key verification. Refusals set ignoreRequest, so callModules does not send a NO_RESPONSE NAK either - otherwise the amplification would just become NAK amplification. The second half is noticing that the mesh has outgrown us. NodeDB now stamps every eviction of a node it had heard within NODEDB_ROLL_FRESH_SECS (2h); NODEDB_ROLL_SAMPLES (8) of those inside NODEDB_ROLL_WINDOW_MS (30 min) means entries are rolling straight back in rather than aging out, which isNodeDbRolling() reports. Evicting long-silent nodes deliberately does not count - that is ordinary pruning, and counting it would leave a small, healthy mesh permanently "rolling". The flag self-clears once the churn stops. While rolling we also defer the unicast reply, skip greeting newly-heard nodes in MeshService::handleFromRadio, and drop the wantReplies bit from our own scheduled broadcast (current firmware ignores that bit now, but older firmware still answers it). The scheduled broadcast itself always goes out - that is what the deferrals are waiting for. Discovery still converges without broadcast replies: a node that hears an unknown peer unicasts it a NodeInfo with want_response, and that unicast is answered. Also stops recording broadcast requests in the 12h reply-dedup cache. Now that they are never answered, recording them would only suppress a later unicast request from the same node - the one exchange we do still want to answer. New suite test/test_nodeinfo_storm covers the churn detector (fresh vs stale evictions, the partial-ring case) and the four reply outcomes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JD4SJbwgECvr14VdBHjsFp
test_rolling_falseWhenEvictingStaleNodes and test_reply_refusedForUnicastWhileRolling each have exactly 35 characters after the test_ prefix, which matches the Lob key pattern and fails the trunk secret scan in CI.
NomDeTom
force-pushed
the
claude/meshtastic-nodeinfo-storm-gk1wst
branch
from
September 16, 2026 01:07
bb3e6f8 to
f0d4061
Compare
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A
want_responseNodeInfo addressed toNODENUM_BROADCASTasks every node in earshot to answer one packet. That is amplification, and it is how a NodeInfo storm starts: the burst of replies is itself traffic from nodes the listeners have never seen, so each of those senders gets greeted in turn, and on a mesh larger thanMAX_NUM_NODESthe greetings evict the very peers that prompted them.This changes two things.
Broadcast NodeInfo requests are never answered.
NodeInfoModule::allocReply()refuses any NodeInfo request sent to the broadcast address. Unicast requests are still answered, so the targeted exchanges are unaffected: the unknown-node greeting inMeshService::handleFromRadio,ReliableRouter'sPKI_UNKNOWN_PUBKEYresponse to a DM we could not decrypt, andKeyVerificationModule. The refusal setsignoreRequest, soMeshModule::callModulesdoes not send aNO_RESPONSENAK either — otherwise the amplification would simply become NAK amplification.NodeDB tracks the freshness of nodes rolling off the database.
getOrCreateMeshNode's eviction now stamps every eviction of a node we had heard withinNODEDB_ROLL_FRESH_SECS(2 h).NODEDB_ROLL_SAMPLES(8) of those insideNODEDB_ROLL_WINDOW_MS(30 min) means entries are rolling straight back in rather than aging out, whichNodeDB::isNodeDbRolling()reports. Evictions of long-silent nodes deliberately do not count — that is ordinary pruning, and counting it would leave a small, healthy mesh permanently flagged. The flag self-clears once the churn stops.While rolling, we additionally defer the unicast reply, skip greeting newly-heard nodes, and drop the
wantRepliesbit from our own scheduled broadcast (current firmware ignores that bit now, but older firmware still answers it). The scheduled broadcast itself always goes out — that is what the deferrals are waiting for.Also stops recording broadcast requests in the existing 12 h reply-dedup cache. Now that they are never answered, recording them would only suppress a later unicast request from the same node, which is the one exchange we do still want to answer.
Discovery still converges
Without broadcast replies, a node learns about us from our broadcast itself; because it then has no user record for us, its own unknown-node path unicasts us a NodeInfo with
want_response, and that unicast is answered. Asymmetric-NodeInfo cases heal the same way, on the peer's next transmission of anything. The phone/TCPHeartbeat(nonce=1)broadcast that the hardware mesh tests rely on still causes the peer to discover the sender through that path.Files
src/modules/NodeInfoModule.cpp— reply policy, and the dedup-cache scopingsrc/mesh/NodeDB.{h,cpp}— churn ring,isNodeDbRolling(), eviction hooksrc/mesh/MeshService.cpp— skip the unknown-node greeting while rollingtest/test_nodeinfo_storm/— new suite.github/copilot-instructions.md— churn detection under NodeDB, and a "NodeInfo storm suppression" section under Important Considerations🤝 Attestations
test/test_nodeinfo_storm(7 cases): the churn detector across fresh evictions, stale evictions, and the partial-ring case that must not trip it; plus all four reply outcomes — broadcast refused, unicast answered, unicast deferred while rolling, scheduled broadcast never suppressed../bin/run-tests.sh:RESULT: GREEN 48/48 suites passed, all CLEAN.No hardware testing. I do not have any of the affected devices, so on-device regression testing has not been done and would be welcome from contributors with hardware — particularly a multi-node mesh to confirm that discovery still converges without broadcast replies. Left as a draft for that reason.