From b914dda2f8b1eade21cefa0e66c6a9d307fb050c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20P=C3=A9rez?= Date: Thu, 25 Jun 2026 15:53:43 -0400 Subject: [PATCH] fix: apply OPTIONS filter on all MASTER downlink paths Remove inject-only bypass in _peer_should_receive_dmrd so peers with empty OPTIONS (silent witness) never receive group DMRD. Align peer_listen_slots for simplex with static TG lists. --- .../application/routing/downlink.py | 10 ++++++++-- .../twisted_adapters/udp_hbp.py | 6 +++--- .../test_hbp_repeat_options_filter.py | 20 +++++++++++++++++++ tests/infrastructure/test_proxy_repeat_e2e.py | 8 +++++--- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/adn_server/application/routing/downlink.py b/src/adn_server/application/routing/downlink.py index 0242d4e..e447e2c 100644 --- a/src/adn_server/application/routing/downlink.py +++ b/src/adn_server/application/routing/downlink.py @@ -90,9 +90,15 @@ def peer_listen_slots(peer: dict[str, Any], tgid: int) -> list[int]: """Voice slots where this peer listens for ``tgid`` (static OPTIONS or wire fallback).""" from adn_server.application.report.payloads import parse_peer_options_static - if peer_is_simplex(peer): - return [SIMPLEX_VOICE_SLOT] ts1, ts2 = parse_peer_options_static(peer.get("OPTIONS")) + if peer_is_simplex(peer): + tg = str(tgid) + if tg in ts1 or tg in ts2: + return [SIMPLEX_VOICE_SLOT] + static = peer_options_static_tg_slot(peer, tgid) + if static is not None: + return [static] + return [] tg = str(tgid) in_ts1 = tg in ts1 in_ts2 = tg in ts2 diff --git a/src/adn_server/infrastructure/twisted_adapters/udp_hbp.py b/src/adn_server/infrastructure/twisted_adapters/udp_hbp.py index 66b476a..590264d 100644 --- a/src/adn_server/infrastructure/twisted_adapters/udp_hbp.py +++ b/src/adn_server/infrastructure/twisted_adapters/udp_hbp.py @@ -549,13 +549,13 @@ class HBPProtocol(DatagramProtocol): return True def _peer_should_receive_dmrd(self, peer_id: bytes, packet: bytes) -> bool: - if not self._inject_multi_peer_options_filter(): - return True if peer_id not in self._peers: return False parsed = parse_dmrd_route_fields(packet) if parsed is None: - return self._cached_connected_peer_count() <= 1 + if self._inject_multi_peer_options_filter(): + return self._cached_connected_peer_count() <= 1 + return True slot, tgid, call_type = parsed if call_type not in ("group", "vcsbk"): return True diff --git a/tests/infrastructure/test_hbp_repeat_options_filter.py b/tests/infrastructure/test_hbp_repeat_options_filter.py index 162abea..ba21cf3 100644 --- a/tests/infrastructure/test_hbp_repeat_options_filter.py +++ b/tests/infrastructure/test_hbp_repeat_options_filter.py @@ -73,6 +73,26 @@ def test_repeat_only_reaches_peers_with_matching_options() -> None: assert other_pkts == [] +def test_standalone_master_blocks_empty_options_witness() -> None: + """Bridge MASTER without inject-only index still filters by OPTIONS (silent witness).""" + stack = build_hbp_repeat_stack(talker_alias=False, system_name="MASTER-A") + silent = bytes_4(730039263) + addr_silent = ("10.0.0.13", 62013) + stack.register_peer(_PEER_TX, _ADDR_TX, options=f"TS2={_TG};") + stack.register_peer(_PEER_RX_MATCH, _ADDR_MATCH, options=f"TS2={_TG};") + stack.register_peer(silent, addr_silent, options="SINGLE=0;") + stack.hbp._refresh_connected_peer_count() + assert not stack.hbp._inject_multi_peer_options_filter() + + stack.transport.clear() + stack.hbp.send_peers(_voice_burst()) + + match_pkts = [p for p in stack.transport.for_addr(_ADDR_MATCH) if p[:4] == DMRD] + silent_pkts = [p for p in stack.transport.for_addr(addr_silent) if p[:4] == DMRD] + assert len(match_pkts) == 1 + assert silent_pkts == [] + + def test_bridge_downlink_after_obp_tx_stamp_reaches_matching_peer() -> None: """OBP→MASTER send_peers must deliver when bridge leg stamped TX on STATUS.""" stack = _inject_proxy_stack() diff --git a/tests/infrastructure/test_proxy_repeat_e2e.py b/tests/infrastructure/test_proxy_repeat_e2e.py index 857fb56..00bc73e 100644 --- a/tests/infrastructure/test_proxy_repeat_e2e.py +++ b/tests/infrastructure/test_proxy_repeat_e2e.py @@ -50,8 +50,10 @@ _EMB_SLICE = slice(116, 148) def _proxy_fanin_stack() -> tuple[ProxyFanInProtocol, HbpRepeatStack]: stack = build_hbp_repeat_stack(talker_alias=True, system_name="MASTER-A") - stack.register_peer(_PEER_TX, _ADDR_TX) - stack.register_peer(_PEER_RX, _ADDR_RX) + stack.config["PROXY"] = {"TARGET_SYSTEM": "MASTER-A"} + stack.hbp._CONFIG = stack.config + stack.register_peer(_PEER_TX, _ADDR_TX, options="TS2=7304;") + stack.register_peer(_PEER_RX, _ADDR_RX, options="TS2=7304;") proxy = ProxyUseCases( InMemoryProxySlotStore(), @@ -98,7 +100,7 @@ def test_proxy_attach_binds_sockaddr_used_for_master_ingress() -> None: fanin, stack = _proxy_fanin_stack() ensure_system_runtime_config(stack.config) packet = DeterministicScenario.voice_head_spec( - PacketSpec(peer_id=730039210, stream_id=0x55667788) + PacketSpec(peer_id=730039210, dst_id=7304, stream_id=0x55667788) ).data() fanin.datagramReceived(packet, _ADDR_TX)