From 1bbd2156a17440949123913db36d2cabcdea6c14 Mon Sep 17 00:00:00 2001 From: agessaman Date: Sun, 19 Jul 2026 07:11:49 -0700 Subject: [PATCH] fix(trace): gate intermediate-hop trace relay on forward mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- repeater/handler_helpers/trace.py | 21 +++++- ...t_handler_helpers_trace_discovery_login.py | 64 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/repeater/handler_helpers/trace.py b/repeater/handler_helpers/trace.py index d90a4c4..c1acf40 100644 --- a/repeater/handler_helpers/trace.py +++ b/repeater/handler_helpers/trace.py @@ -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: diff --git a/tests/test_handler_helpers_trace_discovery_login.py b/tests/test_handler_helpers_trace_discovery_login.py index 9333f3b..ff9aa2d 100644 --- a/tests/test_handler_helpers_trace_discovery_login.py +++ b/tests/test_handler_helpers_trace_discovery_login.py @@ -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()