fix(trace): gate intermediate-hop trace relay on forward mode

Firmware only relays a TRACE at an intermediate hop when
allowPacketForward passes, so disabling forwarding stops trace relay.
The trace helper forwarded via packet injection, which is gated only by
the local-TX check — a repeater in monitor mode kept repeating traces
while reporting repeat off. Consult the repeater mode before relaying;
locally originated pings are injected directly and keep working in
monitor mode, and ping-response matching still runs before the relay
decision.
This commit is contained in:
agessaman
2026-07-19 07:11:49 -07:00
parent f19421ee6c
commit 1bbd2156a1
2 changed files with 84 additions and 1 deletions
+20 -1
View File
@@ -135,11 +135,16 @@ class TraceHelper:
logger.info(f"Path SNRs: [{', '.join(path_snrs)}], Hashes: [{', '.join(path_hashes)}]")
should_forward = self._should_forward_trace(packet, trace_bytes, flags, hash_width)
mode_blocked = should_forward and not self._relay_allowed()
if mode_blocked:
should_forward = False
logger.info("Trace relay suppressed (repeat disabled)")
if should_forward:
await self._forward_trace_packet(packet, num_hops)
else:
self._log_no_forward_reason(packet, trace_bytes, hash_width)
if not mode_blocked:
self._log_no_forward_reason(packet, trace_bytes, hash_width)
if (
self.on_trace_complete
and self._is_trace_complete(packet, trace_bytes, hash_width)
@@ -271,6 +276,20 @@ class TraceHelper:
return path_snrs, path_hashes
def _relay_allowed(self) -> bool:
"""Firmware gates intermediate-hop TRACE relay on ``allowPacketForward``
(Mesh.cpp), so a repeater that is not forwarding (monitor / no_tx mode)
must not relay traces. Locally originated pings are injected directly
and never pass through this gate.
"""
config = getattr(self.repeater_handler, "config", None)
mode = "forward"
if isinstance(config, dict):
mode = config.get("repeater", {}).get("mode", "forward")
if mode not in ("forward", "monitor", "no_tx"):
mode = "forward"
return mode == "forward"
def _should_forward_trace(
self, packet, trace_bytes: bytes, flags: int, hash_width: int
) -> bool:
@@ -201,6 +201,70 @@ async def test_trace_helper_forward_trace_packet_updates_recent_record_and_injec
packet_injector.assert_awaited_once()
def _trace_relay_helper(mode, forward=True):
"""TraceHelper wired so _should_forward_trace says yes; mode comes from config."""
repeater_handler = MagicMock()
repeater_handler.is_duplicate.return_value = False
repeater_handler.calculate_packet_score.return_value = 0.9
repeater_handler.config = {"repeater": {"mode": mode}}
helper = TraceHelper(
local_hash=0x42,
local_identity=FakeIdentity(0x42),
repeater_handler=repeater_handler,
)
helper._forward_trace_packet = AsyncMock()
helper._extract_path_info = MagicMock(return_value=([], []))
helper._should_forward_trace = MagicMock(return_value=forward)
helper.trace_handler._parse_trace_payload = MagicMock(
return_value={
"valid": True,
"trace_path_bytes": b"\x42\x43",
"flags": 0,
"trace_hops": [b"\x42", b"\x43"],
"trace_path": [0x42, 0x43],
"tag": 1234,
}
)
helper.trace_handler._format_trace_response = MagicMock(return_value="trace ok")
return helper
@pytest.mark.asyncio
async def test_trace_relay_suppressed_in_monitor_and_no_tx_mode():
# Firmware gates TRACE relay on allowPacketForward, so a repeater with
# repeat off must not relay an intermediate-hop trace.
for mode in ("monitor", "no_tx"):
helper = _trace_relay_helper(mode)
packet = DummyPacket(path=b"\x01", payload=b"\xaa\xbb\xcc")
await helper.process_trace_packet(packet)
helper._forward_trace_packet.assert_not_awaited()
@pytest.mark.asyncio
async def test_trace_relay_allowed_in_forward_and_unknown_mode():
for mode in ("forward", "weird-mode"):
helper = _trace_relay_helper(mode)
packet = DummyPacket(path=b"\x01", payload=b"\xaa\xbb\xcc")
await helper.process_trace_packet(packet)
helper._forward_trace_packet.assert_awaited_once()
@pytest.mark.asyncio
async def test_trace_ping_response_still_matches_in_monitor_mode():
# Ping origination/response handling is not relay: a pending ping must
# resolve even when the repeater is not forwarding.
helper = _trace_relay_helper("monitor", forward=False)
tag = 555
helper.trace_handler._parse_trace_payload.return_value["tag"] = tag
evt = helper.register_ping(tag, 0x42)
packet = DummyPacket(path=b"\x01", payload=b"\xaa\xbb\xcc")
await helper.process_trace_packet(packet)
assert evt.is_set()
helper._forward_trace_packet.assert_not_awaited()
def test_trace_helper_cleanup_stale_pings():
helper = TraceHelper(
local_hash=0x42, local_identity=FakeIdentity(0x42), repeater_handler=MagicMock()