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/src/adn_server/application/proxy/use_cases.py

154 lines
5.7 KiB

# ADN DMR Peer Server - application proxy use cases
#
# 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
###############################################################################
"""ProxyService use cases: client registration, pending RPTO (Phase 3)."""
from __future__ import annotations
import time
from collections.abc import Sequence
from adn_server.application.ports import PendingRptoQueue, ProxyIpBlacklist, ProxySlotStore
from adn_server.domain.errors import DomainError
from adn_server.domain.proxy import ClientEndpoint, ClientSlot, SessionTeardown
from adn_server.domain.result import Fail, Result, Success
from adn_server.domain.value_objects import int_id
class ProxySlotError(DomainError):
"""Proxy session allocation or lookup failure."""
class ProxyUseCases:
"""Register hotspot clients and queue RPTO for the master (in-process inject)."""
def __init__(
self,
slot_store: ProxySlotStore,
rpto_queue: PendingRptoQueue,
*,
max_peers: int = 1,
black_list: Sequence[int] = (),
ip_blacklist: ProxyIpBlacklist | None = None,
) -> None:
self._slots = slot_store
self._rpto_queue = rpto_queue
self._max_peers = max_peers
self._black_list = frozenset(black_list)
self._ip_blacklist = ip_blacklist
def _allocate_report_slot(self) -> int | None:
"""Lowest free upstream slot index (legacy adn-proxy ``connTrack`` port pool)."""
used = {
slot.report_slot
for slot in self._slots.list_slots()
if slot.report_slot is not None
}
for index in range(self._max_peers):
if index not in used:
return index
return None
def attach_client(
self,
peer_id: bytes,
host: str,
port: int,
) -> Result[ClientSlot, ProxySlotError]:
"""Bind or refresh a hotspot session (legacy ``peer_track`` on client packet)."""
if len(peer_id) != 4:
return Fail(ProxySlotError("peer_id must be 4 bytes"))
if self.is_ip_blocked(host):
return Fail(ProxySlotError("client IP is blacklisted"))
existing = self._slots.get_by_peer(peer_id)
if existing is not None:
updated = existing.with_client(host, port)
self._slots.update_client(peer_id, host, port)
return Success(updated)
if int_id(peer_id) in self._black_list:
return Fail(ProxySlotError("peer is blacklisted"))
if len(self._slots.list_slots()) >= self._max_peers:
return Fail(ProxySlotError("maximum peers exceeded"))
report_slot = self._allocate_report_slot()
slot = ClientSlot(
peer_id=peer_id,
client=ClientEndpoint(host=host, port=port),
report_slot=report_slot,
)
self._slots.bind(slot)
return Success(slot)
def detach_client(self, peer_id: bytes) -> ClientSlot | None:
"""Release session (legacy ``reaper`` slot drop without I/O)."""
return self._slots.unbind(peer_id)
def expire_session(self, peer_id: bytes) -> SessionTeardown | None:
"""End session and return teardown plan for infrastructure I/O (legacy ``reaper``)."""
slot = self.detach_client(peer_id)
if slot is None:
return None
return SessionTeardown(peer_id=slot.peer_id, client=slot.client)
def is_ip_blocked(self, host: str, now: float | None = None) -> bool:
if self._ip_blacklist is None:
return False
return self._ip_blacklist.is_blocked(host, now if now is not None else time.time())
def block_ip_until(self, host: str, expire_at: float) -> None:
if self._ip_blacklist is not None:
self._ip_blacklist.block_until(host, expire_at)
def block_ip_from_prbl(
self,
data: bytes,
host: str,
*,
default_ttl: float = 300,
now: float | None = None,
) -> float:
"""Parse PRBL expiry and block client IP (legacy ``proxy`` PRBL handler)."""
ts = now if now is not None else time.time()
expire = ts + default_ttl
if len(data) > 8:
try:
expire = float(data[8:].decode("utf-8"))
except (ValueError, UnicodeDecodeError):
pass
self.block_ip_until(host, expire)
return expire
def resolve_client(self, peer_id: bytes) -> ClientEndpoint | None:
"""Client endpoint for a connected peer."""
slot = self._slots.get_by_peer(peer_id)
return slot.client if slot else None
def list_slots(self) -> tuple[ClientSlot, ...]:
return self._slots.list_slots()
def apply_runtime_settings(
self,
*,
max_peers: int,
black_list: Sequence[int],
) -> None:
"""Hot-reload proxy limits without dropping active sessions."""
self._max_peers = max_peers
self._black_list = frozenset(black_list)

Powered by TurnKey Linux.