Skip to content

fix(MeshService): address the rate-limit NAK with getFrom(), not mp->from - #11446

Open
h3lix1 wants to merge 7 commits into
meshtastic:developfrom
h3lix1:fix/routing-error-response-getfrom
Open

h3lix1 wants to merge 7 commits into
meshtastic:developfrom
h3lix1:fix/routing-error-response-getfrom

Conversation

@h3lix1

@h3lix1 h3lix1 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

MeshService::sendRoutingErrorResponse() passes mp->from straight through as the NAK destination.
Every other ack/nak call site uses getFrom(), which turns the unset sentinel 0 into our own node
number.

The only caller is the 2 second TEXT_MESSAGE_APP rate limit at PhoneAPI.cpp:1865. That path
returns false before MeshService::handleToRadio() reaches its p.from = 0 normalisation, so
from is whatever the client put in the ToRadio packet.

If the client left it unset, to on the NAK is 0. isToUs(0) is false so Router::sendLocal()
does not deliver it to the phone, and the client gets no ROUTING_APP response at all for the
message that was just rejected, only a QueueStatus. The NAK is instead encrypted and transmitted
over LoRa addressed to node 0. It goes out with the default hopLimit of 0 so nothing rebroadcasts
it, which keeps the cost to one wasted transmission plus a client left without a verdict.

Since getFrom() only rewrites 0, none of this shows up when the client sets from to its own node
number, which both first party clients normally do. On Android CommandSenderImpl.kt uses
from = nodeManager.myNodeNum.value ?: 0, so it degrades to 0 during the window before myNodeNum
is populated. I have not reproduced that: it needs two text sends inside the 2 second window during
startup. The convention of leaving from unset is real and used by first party code, for instance
AccessoryManager+Lockdown.swift carries the comment "meshPacket.from intentionally NOT set. Proto
default 0 means firmware...". That path does not send text so it cannot trigger this rate limit, but
it does show unset from is expected rather than malformed.

Fix is getFrom(mp), matching the other call sites. No behaviour change when from is already set.

sendRoutingErrorResponse() had no native coverage before this change; test_mesh_module now
carries three cases for it. test_routingErrorResponse_unsetFromAddressesUs is the one that holds
the fix in place: it builds the request the rate limit rejects with from unset and asserts the NAK
is addressed to us rather than node 0, plus the request id and channel the client needs to match the
verdict to its message. Reverting to mp->from fails it with Expected 286331153 Was 0.

The other two state what the fix does not change, and pass either way: a sender that is already set
is passed through untouched, and a null packet generates no NAK. Suite is 27/27.

Testing: builds clean on heltec-v4 and seeed-xiao-s3, targeted native suites unchanged. Not
exercised on hardware, since the CLI sets from and I could not construct the failing case with it.

🤖 Generated with Claude Code

🤝 Attestations

  • I have tested that my proposed changes behave as described.
  • 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: compile-only on Heltec V4 and Seeed XIAO ESP32-S3

Summary by CodeRabbit

  • Bug Fixes
    • Corrected routing error responses so acknowledgments and negative acknowledgments reach the intended destination, including local requests.
    • Preserved sender information when it is already set.
    • Prevented responses from being generated for missing packets.
    • Ensured error responses retain the relevant error, request, and channel details.
    • Improved reliability for routing error handling across local and remote requests.

…from

sendRoutingErrorResponse() passes mp->from straight through as the NAK's
destination. Every other ack/nak call site in the tree uses getFrom(), which
resolves the "unset" sentinel 0 to our own node number.

The single caller is PhoneAPI's 2-second TEXT_MESSAGE_APP rate limit
(PhoneAPI.cpp:1865). That path returns false before MeshService::handleToRadio()
reaches its `p.from = 0` normalisation, so `from` is whatever the client put
there. A client that leaves it unset gets:

  - to == 0 on the NAK, so isToUs() is false and sendLocal() does not deliver
    it to the phone - the client receives no ROUTING_APP response at all for
    the message it just had rejected, only a QueueStatus;
  - the NAK encrypted and transmitted over LoRa addressed to node 0.

Both current first-party clients do set `from` (Android uses myNodeNum with a
0 fallback, Apple sets deviceNum outside its Lockdown path), so this is
reachable via an early-connection race before myNodeNum is known, Apple's
Lockdown path, and third-party clients.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 20168ee5-dd01-4577-a84a-b8b07b7568da

📥 Commits

Reviewing files that changed from the base of the PR and between 149af9d and 7e5f46a.

📒 Files selected for processing (1)
  • src/mesh/MeshService.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/mesh/MeshService.cpp

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

sendRoutingErrorResponse now resolves the response destination with getFrom(mp). Tests cover unset and set senders, response fields, and null packets.

Changes

Routing error response handling

Layer / File(s) Summary
Resolve and validate routing error destinations
src/mesh/MeshService.cpp, test/test_mesh_module/test_main.cpp
sendRoutingErrorResponse uses getFrom(mp). Tests verify local delivery for an unset sender, pass-through for a set sender, required NAK fields, and no response for a null packet.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~8 minutes

Change: Bug fix

Suggested reviewers: rcgv1

Merge Risk: ⚪ Minimal · up to f295e

The routing-error destination change is covered by native tests for unset, explicit, and null packet cases, with no unresolved merge risk identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary fix: using getFrom() instead of mp->from for rate-limit NAK addressing.
Description check ✅ Passed The description explains the bug, affected flow, implementation, regression tests, build results, and hardware-testing limitation. It also includes the required attestation section and records the rel…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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.

@h3lix1
h3lix1 force-pushed the fix/routing-error-response-getfrom branch from 6f965b6 to d5272e9 Compare August 13, 2026 20:17

@RCGV1 RCGV1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I independently traced the unset-from rate-limit path and agree with this fix. sendRoutingErrorResponse() is reached before handleToRadio() normalizes phone-originated packets, so using mp->from can address the NAK to node 0. getFrom(mp) is the correct normalization and matches the other ACK/NAK call sites.

I also exercised a focused native regression that constructs a phone request with from = 0, sends RATE_LIMIT_EXCEEDED, and asserts the generated ACK/NAK targets the local node while preserving the request ID and channel. It passes on this PR head; the complete test_mesh_module suite passed 25/25.

I recommend adding that regression to this PR so the pre-normalization behavior stays covered. The essential assertion is that ackNaks[0].to == LOCAL_NODE after calling sendRoutingErrorResponse(..., &request) with request.from = 0.

sendRoutingErrorResponse() had no native coverage before or after the fix, so
nothing held the getFrom() call in place.

test_routingErrorResponse_unsetFromAddressesUs builds the phone request the
2 second TEXT_MESSAGE_APP rate limit rejects, with from left unset as a client
may send it and as handleToRadio() has not yet normalised it, and asserts the
NAK is addressed to us rather than to node 0. It also pins the request id and
channel, which are what let the client match the verdict to the message it
just sent.

Two more hold the surrounding contract: a request that already carries a sender
is passed through untouched, since getFrom() only rewrites 0, and a null packet
generates no NAK at all.

Reverting the fix to mp->from fails the first with
"Expected 286331153 Was 0", 0x11111111 being the harness's local node. The
other two pass either way, which is the intent: they describe behaviour the fix
does not change.

Regression suggested by RCGV1, who traced the same path independently.
@h3lix1

h3lix1 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, that is a useful review, and the regression is in as 6fbd1d9b9.

Your reading of the ordering is right, and the assertion you named is the one that discriminates. Reverting the fix to mp->from fails it with Expected 286331153 Was 0, 0x11111111 being the harness's LOCAL_NODE, so it does hold the fix in place rather than passing either way.

I added the id and channel assertions you mentioned for the same reason: those are what let the client match the verdict to the message it just sent, so a NAK that arrives correctly addressed but unmatchable would still leave the client without an answer.

Two more went in alongside it, to state the parts the fix does not change:

  • test_routingErrorResponse_setFromIsPassedThrough, since getFrom() only rewrites 0 and the PR claims no behaviour change when the sender is already set. Worth pinning rather than asserting in prose.
  • test_routingErrorResponse_nullPacketGeneratesNothing, covering the existing null guard.

Both pass with or without the fix, which is deliberate: they describe the surrounding contract, and only the unset-from case is evidence for the change itself.

test_mesh_module is 27/27 under the coverage environment. The count differs from your 25 because of the three added here.

One correction to the PR description, which I will update: it says there is no native coverage for this path either before or after, which was true when I wrote it and is now only true of "before".

@h3lix1
h3lix1 marked this pull request as ready for review September 10, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants