You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ADN-DMR-Peer-Server/tests/infrastructure/test_hbp_private_call_targe...

155 lines
6.8 KiB

# ADN DMR Peer Server - tests infrastructure hbp private call targeting
#
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
#
###############################################################################
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
"""SUB_MAP-based precise private-call targeting on a MASTER with 3+ peers.
Legacy (hblink.py master_datagramReceived) broadcasts every private call to
every connected peer unconditionally -- the receiving hotspot's own ACL/sub
list is the only filter, so a private call between two unrelated users still
reaches every other hotspot's air interface. _pvt_repeat_targets narrows this:
once SUB_MAP has recorded which peer_id last carried a given subscriber's
traffic, a private call to that subscriber is delivered only to that peer,
never broadcast. Unknown destinations still fall back to broadcast (legacy
parity) -- see test_hbp_repeat_private_call.py."""
from __future__ import annotations
import dataclasses
from tests.harness.deterministic import DeterministicScenario, PacketSpec
from tests.support.hbp_repeat_stack import build_hbp_repeat_stack
from adn_server.domain import bytes_3, bytes_4
from adn_server.infrastructure.hbp_constants import RPTC, RPTK, RPTL
from adn_server.infrastructure.twisted_adapters.udp_hbp import _calc_hash, _get_passphrase_bytes
_PEER_TX = bytes_4(730039210)
_PEER_RX = bytes_4(730039101)
_PEER_OTHER = bytes_4(730039199)
_ADDR_TX = ("10.0.0.1", 62001)
_ADDR_RX = ("10.0.0.2", 62002)
_ADDR_OTHER = ("10.0.0.3", 62003)
_DST_SUB = 7304011
def _private_spec() -> PacketSpec:
return PacketSpec(
peer_id=730039210,
rf_src=7300392,
dst_id=_DST_SUB,
slot=2,
call_type="unit",
stream_id=0xA1B2C3D4,
payload=b"\x00" * 33,
)
def _fire_private_call(stack, base: PacketSpec) -> None:
stack.inject_spec(DeterministicScenario.voice_head_spec(base), _ADDR_TX)
stack.transport.clear()
stack.inject_spec(
DeterministicScenario.voice_burst_spec(base, seq=1, dtype_vseq=1), _ADDR_TX,
)
def test_private_call_delivered_only_to_last_known_peer_not_broadcast() -> None:
stack = build_hbp_repeat_stack(talker_alias=True)
stack.register_peer(_PEER_TX, _ADDR_TX, options="TS2=7304;")
stack.register_peer(_PEER_RX, _ADDR_RX, options="TS2=7304;")
stack.register_peer(_PEER_OTHER, _ADDR_OTHER, options="TS2=7304;")
stack.config["_SUB_MAP"] = {
bytes_3(_DST_SUB): (stack.system_name, 2, 1_700_000_000.0, _PEER_RX),
}
_fire_private_call(stack, _private_spec())
assert stack.transport.for_addr(_ADDR_RX), "known peer must still receive the call"
assert not stack.transport.for_addr(_ADDR_OTHER), "uninvolved peer must not see this private call"
def test_private_call_dropped_when_known_peer_no_longer_connected() -> None:
stack = build_hbp_repeat_stack(talker_alias=True)
stack.register_peer(_PEER_TX, _ADDR_TX, options="TS2=7304;")
stack.register_peer(_PEER_OTHER, _ADDR_OTHER, options="TS2=7304;")
# _PEER_RX was last heard here, but is no longer registered/connected.
stack.config["_SUB_MAP"] = {
bytes_3(_DST_SUB): (stack.system_name, 2, 1_700_000_000.0, _PEER_RX),
}
_fire_private_call(stack, _private_spec())
assert not stack.transport.for_addr(_ADDR_OTHER), "must not blast to peers that can't be the destination"
def test_private_call_not_delivered_locally_when_known_on_different_system() -> None:
stack = build_hbp_repeat_stack(talker_alias=True)
stack.register_peer(_PEER_TX, _ADDR_TX, options="TS2=7304;")
stack.register_peer(_PEER_OTHER, _ADDR_OTHER, options="TS2=7304;")
stack.config["_SUB_MAP"] = {
bytes_3(_DST_SUB): ("MASTER-B", 2, 1_700_000_000.0, _PEER_RX),
}
_fire_private_call(stack, _private_spec())
assert not stack.transport.for_addr(_ADDR_OTHER), "destination known elsewhere must not repeat locally"
def test_private_call_to_4000_not_broadcast() -> None:
"""TG/ID 4000 is the dynamic-TG reset control code, never a real subscriber --
it can never appear in _SUB_MAP, so without a special case it always fell into
the "unknown destination" broadcast fallback and reached every connected peer
instead of being stopped at the server (dmrd_received's own dst_id == 4000
guard runs too late: _pvt_repeat_targets/the REPEAT loop already ran)."""
stack = build_hbp_repeat_stack(talker_alias=True)
stack.register_peer(_PEER_TX, _ADDR_TX, options="TS2=7304;")
stack.register_peer(_PEER_RX, _ADDR_RX, options="TS2=7304;")
stack.register_peer(_PEER_OTHER, _ADDR_OTHER, options="TS2=7304;")
base = dataclasses.replace(_private_spec(), dst_id=4000)
_fire_private_call(stack, base)
assert not stack.transport.for_addr(_ADDR_RX), "TG 4000 must not be broadcast to any peer"
assert not stack.transport.for_addr(_ADDR_OTHER), "TG 4000 must not be broadcast to any peer"
def test_sub_map_entries_for_peer_purged_on_reconnect() -> None:
"""Once a hotspot finishes (re)logging in, any SUB_MAP entry pointing at it is
stale -- the radio may have moved to a different hotspot while this one was
offline and there's no way to tell without hearing it transmit again."""
stack = build_hbp_repeat_stack(talker_alias=True)
stack.hbp._config["PASSPHRASE"] = b"test-passphrase"
stack.config["_SUB_MAP"] = {
bytes_3(_DST_SUB): (stack.system_name, 2, 1_700_000_000.0, _PEER_RX),
bytes_3(1234567): (stack.system_name, 1, 1_700_000_000.0, _PEER_OTHER),
}
passphrase = _get_passphrase_bytes(stack.hbp._config)
stack.hbp.datagramReceived(RPTL + _PEER_RX, _ADDR_RX)
salt = bytes_4(stack.hbp._peers[_PEER_RX]["SALT"])
stack.hbp.datagramReceived(RPTK + _PEER_RX + _calc_hash(salt, passphrase), _ADDR_RX)
stack.hbp.datagramReceived(
RPTC + _PEER_RX + b"CE5RPY " + b"\x00" * 85 + b"4", _ADDR_RX,
)
assert stack.hbp._peers[_PEER_RX]["CONNECTION"] == "YES"
assert bytes_3(_DST_SUB) not in stack.config["_SUB_MAP"], "entry pointing at the reconnected peer must be gone"
assert bytes_3(1234567) in stack.config["_SUB_MAP"], "entries for other peers must be untouched"

Powered by TurnKey Linux.