fix(channels): skip self-echoes of raw resends in _on_channel_message

CMD_SEND_RAW_PACKET bypasses sendFlood()'s _tables->hasSeen() self-mark.
The firmware seen-table is 160 entries in RAM and rolls over in minutes
on a busy mesh, so a resend issued after the original hash got evicted
comes back via repeater echo, fails hasSeen (entry gone), and is
delivered as RESP_CODE_CHANNEL_MSG_RECV. Result: the user's own resend
appears as an "incoming" message from themselves a few minutes later.

Detection: in _on_channel_message compute the expected pkt_payload from
the event's (channel_secret, sender_timestamp, txt_type, raw_text) and
ask the DB if any own row already has that exact hash. If yes, treat as
self-echo and return — no DB insert, no SocketIO emit. Index idx_cm_pkt
keeps the lookup cheap. Guarded with try/except so any detection failure
falls through to normal handling — we never want a check bug to drop
real inbound messages.

Documented as a known limitation in reference_meshcore_raw_packet.md;
this lifts that caveat.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-06-10 19:45:39 +02:00
parent e14c4fab06
commit f57b9474bb
2 changed files with 54 additions and 0 deletions
+18
View File
@@ -1001,6 +1001,24 @@ class Database:
(raw_packet, msg_id)
)
def has_own_channel_message_with_pkt_payload(self, pkt_payload: str) -> bool:
"""Check if we sent a channel message with the given pkt_payload.
Used by the CHANNEL_MSG_RECV handler to detect self-echoes of raw
resends — when CMD_SEND_RAW_PACKET bypasses firmware's hasSeen mark
and an echo from a repeater is processed as a "new" incoming
message. Index `idx_cm_pkt` makes the lookup O(log n).
"""
if not pkt_payload:
return False
with self._connect() as conn:
row = conn.execute(
"SELECT 1 FROM channel_messages "
"WHERE pkt_payload = ? AND is_own = 1 LIMIT 1",
(pkt_payload,)
).fetchone()
return row is not None
# ================================================================
# Paths
# ================================================================
+36
View File
@@ -720,6 +720,42 @@ class DeviceManager:
sender = 'Unknown'
content = raw_text
# Self-echo guard for raw resends.
#
# CMD_SEND_RAW_PACKET (used by /api/messages/<id>/resend) bypasses
# firmware's sendFlood() and therefore the _tables->hasSeen()
# self-mark. The 160-entry seen-table is RAM-only and rolls over
# in minutes on a busy mesh, so a resend issued after the original's
# hash got evicted comes back via repeater echo, passes hasSeen
# ("not seen"), and is delivered to companion as RESP_CODE_CHANNEL_MSG_RECV.
# Without this guard the user sees their own resend pop up as an
# incoming message from themselves.
#
# Compute the same pkt_payload we would have stored for the original
# send and ask the DB if we already have an own row with that hash.
sender_ts = data.get('sender_timestamp')
txt_type = data.get('txt_type', 0)
if sender_ts and sender == self.device_name:
try:
secret = self._channel_secrets.get(channel_idx)
if secret:
expected_pkt_payload = _compute_pkt_payload(
secret, sender_ts, txt_type, raw_text
)
if self.db.has_own_channel_message_with_pkt_payload(
expected_pkt_payload
):
logger.info(
f"Self-echo of own ch{channel_idx} msg detected "
f"(sender_ts={sender_ts}) — stale resend "
f"firmware-loopback. Skipping insertion."
)
return
except Exception as e:
# Don't let detection failures drop legitimate inbound
# messages — fall through to normal handling.
logger.debug(f"Self-echo check inconclusive: {e}")
# Check if sender is blocked (store but don't emit)
blocked_names = self.db.get_blocked_contact_names()
is_blocked = sender in blocked_names