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.
144 lines
5.5 KiB
144 lines
5.5 KiB
# ADN DMR Peer Server - OBP peer anchored to DNS: only a re-resolution may move it
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from types import SimpleNamespace
|
|
|
|
from twisted.internet import defer
|
|
|
|
from adn_server.domain.mesh_session import ObpBridgeSession
|
|
from adn_server.infrastructure.twisted_adapters import udp_hbp
|
|
from adn_server.infrastructure.twisted_adapters.udp_hbp import HBPProtocol
|
|
|
|
_RESOLVE = HBPProtocol._obp_resolve_target
|
|
_RESOLVED = HBPProtocol._obp_target_resolved
|
|
_REJECT = HBPProtocol._obp_reject_source
|
|
|
|
_HOST = "peer.example.net"
|
|
_CONFIGURED = ("74.132.44.239", 62059)
|
|
_MOVED = ("203.0.113.9", 62059)
|
|
_ZOMBIE = ("129.80.176.29", 62059)
|
|
_NOW = 1_800_000_000.0
|
|
|
|
|
|
def _fake_obp(*, dns_host: str | None = _HOST) -> SimpleNamespace:
|
|
fake = SimpleNamespace(
|
|
_system="OBP-USA",
|
|
_config={"MODE": "OPENBRIDGE", "RELAX_CHECKS": True},
|
|
_session=ObpBridgeSession(
|
|
system_name="OBP-USA", configured_peer=_CONFIGURED, dns_host=dns_host
|
|
),
|
|
_obp_foreign_source_seen={},
|
|
)
|
|
fake._obp_resolve_target = lambda: _RESOLVE(fake)
|
|
fake._obp_target_resolved = lambda host: _RESOLVED(fake, host)
|
|
fake._obp_target_resolve_failed = lambda failure: None
|
|
return fake
|
|
|
|
|
|
class _FakeResolver:
|
|
"""Stands in for the reactor: records the names asked for, answers on demand."""
|
|
|
|
def __init__(self, answer: str) -> None:
|
|
self.answer = answer
|
|
self.asked: list[str] = []
|
|
|
|
def resolve(self, name: str):
|
|
self.asked.append(name)
|
|
return defer.succeed(self.answer)
|
|
|
|
|
|
def test_a_bridge_configured_with_an_address_never_asks_dns(monkeypatch) -> None:
|
|
resolver = _FakeResolver(_MOVED[0])
|
|
monkeypatch.setattr(udp_hbp, "reactor", resolver)
|
|
fake = _fake_obp(dns_host=None)
|
|
_RESOLVE(fake)
|
|
assert resolver.asked == []
|
|
|
|
|
|
def test_resolving_is_rate_limited(monkeypatch) -> None:
|
|
"""An unknown source must not be able to drive one lookup per packet."""
|
|
resolver = _FakeResolver(_CONFIGURED[0])
|
|
monkeypatch.setattr(udp_hbp, "reactor", resolver)
|
|
fake = _fake_obp()
|
|
for _ in range(50):
|
|
_RESOLVE(fake)
|
|
assert len(resolver.asked) == 1
|
|
|
|
|
|
def test_the_peer_follows_the_name_when_it_resolves_elsewhere() -> None:
|
|
fake = _fake_obp()
|
|
_RESOLVED(fake, _MOVED[0])
|
|
assert fake._session.peer == _MOVED
|
|
|
|
|
|
def test_the_peer_stays_put_when_the_name_still_resolves_to_it() -> None:
|
|
fake = _fake_obp()
|
|
_RESOLVED(fake, _CONFIGURED[0])
|
|
assert fake._session.peer == _CONFIGURED
|
|
|
|
|
|
def test_a_frame_from_elsewhere_is_refused_and_asks_dns_again(monkeypatch, caplog) -> None:
|
|
resolver = _FakeResolver(_CONFIGURED[0])
|
|
monkeypatch.setattr(udp_hbp, "reactor", resolver)
|
|
fake = _fake_obp()
|
|
with caplog.at_level(logging.INFO, logger="adn_server.infrastructure.twisted_adapters.udp_hbp"):
|
|
_REJECT(fake, b"BCKA", _ZOMBIE)
|
|
assert resolver.asked == [_HOST]
|
|
assert fake._session.peer == _CONFIGURED, "the zombie took the link over"
|
|
assert "discarded" in caplog.text and _HOST in caplog.text
|
|
|
|
|
|
def test_a_refused_source_is_rate_limited_but_counted_every_time(monkeypatch, caplog) -> None:
|
|
"""A clone pinging every 10s must not flood the log, but every frame it sends
|
|
still has to reach the tally."""
|
|
resolver = _FakeResolver(_CONFIGURED[0])
|
|
monkeypatch.setattr(udp_hbp, "reactor", resolver)
|
|
fake = _fake_obp()
|
|
with caplog.at_level(logging.INFO, logger="adn_server.infrastructure.twisted_adapters.udp_hbp"):
|
|
for _ in range(30):
|
|
_REJECT(fake, b"BCKA", _ZOMBIE)
|
|
assert sum("discarded" in line for line in caplog.text.splitlines()) == 1
|
|
assert fake._session.drops == {"source-not-peer": 30}
|
|
|
|
|
|
def test_each_frame_type_from_a_refused_source_is_named(monkeypatch, caplog) -> None:
|
|
"""Keying the cap on the address alone hid every frame type after the first:
|
|
a BCVE arriving a millisecond behind a BCKA was silently dropped."""
|
|
resolver = _FakeResolver(_CONFIGURED[0])
|
|
monkeypatch.setattr(udp_hbp, "reactor", resolver)
|
|
fake = _fake_obp()
|
|
with caplog.at_level(logging.INFO, logger="adn_server.infrastructure.twisted_adapters.udp_hbp"):
|
|
_REJECT(fake, b"BCKA", _ZOMBIE)
|
|
_REJECT(fake, b"BCVE", _ZOMBIE)
|
|
_REJECT(fake, b"DMRD", _ZOMBIE)
|
|
assert "BCKA from" in caplog.text
|
|
assert "BCVE from" in caplog.text
|
|
assert "DMRD from" in caplog.text
|
|
|
|
|
|
def test_a_refused_source_is_named_again_after_the_interval(monkeypatch, caplog) -> None:
|
|
resolver = _FakeResolver(_CONFIGURED[0])
|
|
monkeypatch.setattr(udp_hbp, "reactor", resolver)
|
|
fake = _fake_obp()
|
|
with caplog.at_level(logging.INFO, logger="adn_server.infrastructure.twisted_adapters.udp_hbp"):
|
|
_REJECT(fake, b"BCKA", _ZOMBIE)
|
|
for key in fake._obp_foreign_source_seen: # as if the interval had elapsed
|
|
fake._obp_foreign_source_seen[key] -= udp_hbp.OBP_FOREIGN_LOG_INTERVAL_S + 1
|
|
_REJECT(fake, b"BCKA", _ZOMBIE)
|
|
assert sum("discarded" in line for line in caplog.text.splitlines()) == 2
|
|
|
|
|
|
def test_a_peer_that_really_moved_is_adopted_on_the_next_resolution(monkeypatch) -> None:
|
|
"""The whole point: a frame from an unknown address is refused, but it makes us
|
|
ask the name again — and when the name answers with that address, we migrate."""
|
|
resolver = _FakeResolver(_MOVED[0])
|
|
monkeypatch.setattr(udp_hbp, "reactor", resolver)
|
|
fake = _fake_obp()
|
|
|
|
_REJECT(fake, b"DMRD", _MOVED) # refused, and the lookup runs inline here
|
|
|
|
assert resolver.asked == [_HOST]
|
|
assert fake._session.peer == _MOVED, "the peer did not follow the name"
|