Skip to content

Refuse broadcast NodeInfo requests, and back off on a rolling NodeDB - #11423

Draft
jp-bennett wants to merge 2 commits into
developfrom
claude/meshtastic-nodeinfo-storm-gk1wst
Draft

jp-bennett wants to merge 2 commits into
developfrom
claude/meshtastic-nodeinfo-storm-gk1wst

Conversation

@jp-bennett

@jp-bennett jp-bennett commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

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 senders gets greeted in turn, and on a mesh larger than MAX_NUM_NODES the 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 in MeshService::handleFromRadio, ReliableRouter's PKI_UNKNOWN_PUBKEY response to a DM we could not decrypt, and KeyVerificationModule. The refusal sets ignoreRequest, so MeshModule::callModules does not send a NO_RESPONSE NAK 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 within NODEDB_ROLL_FRESH_SECS (2 h). 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 NodeDB::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 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.

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/TCP Heartbeat(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 scoping
  • src/mesh/NodeDB.{h,cpp} — churn ring, isNodeDbRolling(), eviction hook
  • src/mesh/MeshService.cpp — skip the unknown-node greeting while rolling
  • test/test_nodeinfo_storm/ — new suite
  • .github/copilot-instructions.md — churn detection under NodeDB, and a "NodeInfo storm suppression" section under Important Considerations

🤝 Attestations

  • I have tested that my proposed changes behave as described.
    • New native suite 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.
    • Full native suite via ./bin/run-tests.sh: RESULT: GREEN 48/48 suites passed, all CLEAN.
  • I have tested that my proposed changes do not cause any obvious regressions on the following devices:
    • Heltec (Lora32) V3
    • LilyGo T-Deck
    • LilyGo T-Beam
    • RAK WisBlock 4631
    • Seeed Studio T-1000E tracker card
    • Other (please specify below)

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.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jp-bennett
jp-bennett changed the base branch from master to develop August 12, 2026 01:55
@jp-bennett jp-bennett added the enhancement New feature or request label Aug 12, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Note

Building this pull request… the flash button, badges and supported-board
list will appear here automatically once CI finishes.

claude and others added 2 commits September 16, 2026 01:43
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants