Skip to content

Commit ac9f166

Browse files
authored
Fix: Narrow echo bypass for loop prevention (#25) (#26)
* Fix: Narrow echo bypass for loop prevention (#25) * docs: Update RELEASE_NOTES for v1.4.2 (#25)
1 parent c51462f commit ac9f166

File tree

3 files changed

+105
-2
lines changed

3 files changed

+105
-2
lines changed

RELEASE_NOTES.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1-
# Release v1.4.1
1+
# Release v1.4.2
2+
3+
## 🐛 Bug Fixes
4+
5+
### Echo Bypass is Too Broad (Issue #25)
6+
- **Narrowed Implicit ACK Echo Bypass**: Fixed an issue where the loop prevention bypass was too broad, causing unencrypted routing, position, and telemetry packets to echo back to the node unnecessarily.
7+
- Instead of bypassing loop protection for all packets from the local gateway, the proxy now explicitly checks if the packet is `encrypted` or has a valid `request_id`.
8+
- Administrative/plain packets are properly dropped by the loop tracker to minimize unnecessary RF traffic.
9+
10+
## 🧪 Test Coverage
11+
- Added new test cases in `tests/test_echo_bypass.py` to ensure only correct packet types (encrypted, request_id) explicitly bypass loop prevention.
12+
13+
---
214

15+
**Full Changelog**: https://github.com/LN4CY/mqtt-proxy/compare/v1.4.1...v1.4.2
16+
17+
# Release v1.4.1
318
## 🐛 Bug Fixes
419

520
### "Proxy to Client" Ack Restoration (The "Red X" Fix)

handlers/mqtt.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,17 @@ def _on_message(self, client, userdata, message):
169169
envelope.ParseFromString(message.payload)
170170
if envelope.gateway_id:
171171
if self.node_id and (envelope.gateway_id == self.node_id or envelope.gateway_id == f"!{self.node_id}"):
172-
is_echo = True
172+
packet = envelope.packet
173+
# Only allow echo bypass for packets that are eligible for Implicit ACKs
174+
is_eligible_for_ack = False
175+
176+
if packet.HasField("encrypted") and packet.encrypted:
177+
is_eligible_for_ack = True
178+
elif packet.HasField("decoded") and getattr(packet.decoded, "request_id", 0):
179+
is_eligible_for_ack = True
180+
181+
if is_eligible_for_ack:
182+
is_echo = True
173183
except Exception:
174184
pass
175185

tests/test_echo_bypass.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import os
2+
import sys
3+
from unittest.mock import MagicMock
4+
import pytest
5+
6+
# Add parent directory to path
7+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8+
9+
from handlers.mqtt import MQTTHandler
10+
from meshtastic import mesh_pb2
11+
from meshtastic.protobuf import mqtt_pb2
12+
13+
def test_mqtt_loop_prevention_echo_bypass_encrypted():
14+
config_mock = MagicMock()
15+
handler = MQTTHandler(config_mock, "mynode")
16+
17+
# Simulate a packet that we encrypted
18+
envelope = mqtt_pb2.ServiceEnvelope()
19+
envelope.gateway_id = "!mynode"
20+
envelope.packet.encrypted = b'secret'
21+
22+
msg = MagicMock()
23+
msg.topic = "msh/US/2/e/LongFast/!mynode" # From us
24+
msg.payload = envelope.SerializeToString()
25+
msg.retain = False
26+
27+
callback = MagicMock()
28+
handler.on_message_callback = callback
29+
30+
# Process message - should bypass loop prevention because it's encrypted
31+
handler._on_message(None, None, msg)
32+
33+
callback.assert_called_once()
34+
35+
36+
def test_mqtt_loop_prevention_echo_bypass_request_id():
37+
config_mock = MagicMock()
38+
handler = MQTTHandler(config_mock, "mynode")
39+
40+
# Simulate a packet that has a request_id
41+
envelope = mqtt_pb2.ServiceEnvelope()
42+
envelope.gateway_id = "!mynode"
43+
envelope.packet.decoded.request_id = 1234
44+
45+
msg = MagicMock()
46+
msg.topic = "msh/US/2/e/LongFast/!mynode" # From us
47+
msg.payload = envelope.SerializeToString()
48+
msg.retain = False
49+
50+
callback = MagicMock()
51+
handler.on_message_callback = callback
52+
53+
# Process message - should bypass loop prevention because it has request_id
54+
handler._on_message(None, None, msg)
55+
56+
callback.assert_called_once()
57+
58+
def test_mqtt_loop_prevention_blocks_unencrypted_no_request():
59+
config_mock = MagicMock()
60+
handler = MQTTHandler(config_mock, "mynode")
61+
62+
# Simulate a packet like NodeInfo without encryption or request_id
63+
envelope = mqtt_pb2.ServiceEnvelope()
64+
envelope.gateway_id = "!mynode"
65+
envelope.packet.decoded.portnum = 1 # NODEINFO_APP
66+
67+
msg = MagicMock()
68+
msg.topic = "msh/US/2/e/LongFast/!mynode" # From us
69+
msg.payload = envelope.SerializeToString()
70+
msg.retain = False
71+
72+
callback = MagicMock()
73+
handler.on_message_callback = callback
74+
75+
# Process message - should be blocked by loop prevention
76+
handler._on_message(None, None, msg)
77+
78+
callback.assert_not_called()

0 commit comments

Comments
 (0)