Skip to content

fix(security): restore implicit ACK for opaque relays of our own PKI DMs - #11503

Closed
Ixitxachitl wants to merge 1 commit into
meshtastic:developfrom
Ixitxachitl:fix/opaque-relay-implicit-ack
Closed

fix(security): restore implicit ACK for opaque relays of our own PKI DMs#11503
Ixitxachitl wants to merge 1 commit into
meshtastic:developfrom
Ixitxachitl:fix/opaque-relay-implicit-ack

Conversation

@Ixitxachitl

Copy link
Copy Markdown
Contributor

Summary

#10967 ("feat(security): enforce packet authenticity policies") added a passesRoutingAuthGate() step ahead of shouldFilterReceived(). Packets that decode as DECODE_OPAQUE are routed straight to relayOpaquePacket() and returned early — they never reach shouldFilterReceived() at all.

That's the right call for genuinely unauthenticated third-party traffic: we shouldn't admit packets we can't make sense of to PacketHistory, module dispatch, MQTT, or the NextHop/Flooding dedup state.

But it also silently swallows a case that isn't third-party traffic: a relay of our own outgoing PKI DM. The originating node is never the recipient, so it can never PKI-decrypt its own DM — from its own point of view that packet always decodes as opaque. Before #10967, every packet (opaque or not) reached shouldFilterReceived(), whose p->from == getNodeNum() check would match the rebroadcast against our own pending-retransmission map and generate the implicit ACK / cancel the retransmission. After #10967, opaque packets never reach that check, so every PKI DM now pays its full retry cost even when a relay has already carried it onward — the implicit-ACK optimization (and the "stop retransmitting once someone relays it" behavior) is gone specifically for PKI DMs.

Fix

Rather than widening the opaque-relay path itself (which would reopen the unauthenticated-traffic surface #10967 closed — shouldFilterReceived()'s NextHop/Flooding paths do real stopRetransmission/dedup/history work keyed only on the plaintext header), this adds a narrow, purpose-built hook:

  • Router::noteOpaqueOwnRebroadcast() — new virtual, no-op by default.
  • ReliableRouter::noteOpaqueOwnRebroadcast() — overrides it, calling a new handleOwnRebroadcastImplicitAck() helper extracted from the existing p->from == getNodeNum() block in ReliableRouter::shouldFilterReceived(). Same check, same plaintext-header-only authentication boundary that's always backed it, broadcast or unicast.
  • Router::perhapsHandleReceived() calls this new hook for OPAQUE_RELAY_ONLY packets, right after relayOpaquePacket().

The packet itself still never touches PacketHistory, modules, MQTT, or the NextHop/Flooding dedup logic — only the implicit-ACK match, exactly as it worked before #10967.

Testing

  • Added test_C17_opaque_own_rebroadcast_still_generates_implicit_ack alongside the existing test_C* opaque/authenticity group, covering both the restored implicit ACK/retransmission-cancel and the continued absence of history/module/MQTT admission.
  • ./bin/run-tests.sh — full suite green (58/58 suites).
  • ./bin/run-tests.sh -f test_packet_signing, -f test_nexthop_routing, -f test_mqtt — all clean.

🤝 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 (please specify below)

Tested on a Seeed Studio Wio Tracker L1 only — sending a PKI DM and observing the retransmission get canceled once a relay is overheard, matching pre-#10967 behavior. Not yet tested against any other hardware target; the change is router-logic-only (no platform-specific code), but independent confirmation on other boards is welcome before merge.

… implicit ACK

meshtastic#10967 (packet authenticity policies) routes DECODE_OPAQUE packets straight
to relayOpaquePacket() and returns, so they never reach shouldFilterReceived().
That's correct for genuinely unauthenticated third-party traffic, but a relay
of our own outgoing PKI DM also decodes as opaque from the originating node's
point of view (we're never the recipient, so we can never PKI-decrypt it).
The result: hearing our own PKI DM get rebroadcast no longer cancels the
retransmission or generates the local implicit ACK, so every PKI DM now pays
the full retry cost even when a relay already carried it onward.

Route this specific case through a new narrow hook, noteOpaqueOwnRebroadcast(),
instead of widening the opaque path itself. It re-runs only the existing
p->from == getNodeNum() plaintext-header match against our own pending
retransmissions (extracted from ReliableRouter::shouldFilterReceived into
handleOwnRebroadcastImplicitAck() so both paths share it) - the same
authentication boundary that check has always used, broadcast or not. It does
not admit the opaque packet to PacketHistory, module dispatch, MQTT, or the
NextHop/Flooding dedup state that shouldFilterReceived() also drives, so the
unauthenticated-traffic surface meshtastic#10967 closed for third-party opaque packets
stays closed.

Adds test_C17 alongside the existing C-group opaque/authenticity tests,
covering both the restored implicit ACK/cancellation and the absence of any
wider pipeline admission.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 85ba8a37-597a-454a-940a-16a4f26a5c9c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

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.

1 participant