Merge pull request #90 from Amateur-Digital-Network/fix/obp-dns-anchor-host-only

fix(obp): anchor a DNS-named peer by host, not by host and port
pull/91/head
ce5rpy 1 week ago committed by GitHub
commit e76d49716d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -186,12 +186,15 @@ def accepts_source(
) -> bool:
"""A frame counts as ours when it comes from the peer.
RELAX_CHECKS widens that to any address, for a peer on a dynamic IP — but not
when DNS owns the peer, or a second host with the passphrase would pass as it.
A name pins the host, never the port: a peer answers from whatever socket it
bound, which NAT may rewrite and which needs not be the port we send to. With
no name to go by, RELAX_CHECKS decides as it always has.
"""
if addr == session.peer:
return True
return bool(policy.relax_checks) and not session.dns_anchored
if session.dns_anchored:
return bool(addr) and addr[0] == session.peer[0]
return bool(policy.relax_checks)
def _delivery_effects(

@ -93,16 +93,32 @@ class ObpBridgeSession:
# --- peer address --------------------------------------------------------
@property
def _anchor(self) -> tuple[str, int] | None:
"""Where the name puts this peer, once it has resolved to an address at all.
A name that has never resolved anchors nothing: the link has to keep working
on whatever RELAX_CHECKS allows, or a name server that was down at startup
would take it off the air.
"""
if not self.dns_host:
return None
host, port = self.resolved_peer or self.configured_peer
return (host, port) if host else None
@property
def dns_anchored(self) -> bool:
return bool(self.dns_host)
return self._anchor is not None
@property
def peer(self) -> tuple[str | None, int]:
"""Where to send: DNS when it owns this peer, else what the wire taught us."""
if self.dns_anchored:
return self.resolved_peer or self.configured_peer
return self.learned_peer or self.configured_peer
"""Where to send: DNS owns the host, the wire may still refine the port."""
anchor = self._anchor
if anchor is None:
return self.learned_peer or self.configured_peer
if self.learned_peer and self.learned_peer[0] == anchor[0]:
return self.learned_peer
return anchor
@property
def peer_known(self) -> bool:
@ -112,9 +128,10 @@ class ObpBridgeSession:
"""Remember the address a datagram really came from. True when it moved."""
if not addr or not addr[0]:
return False
if self.dns_anchored:
return False
host, port = str(addr[0]), int(addr[1])
anchor = self._anchor
if anchor is not None and host != anchor[0]:
return False # only a re-resolution moves an anchored peer to another host
if self.peer == (host, port):
return False
self.learned_peer = (host, port)
@ -125,10 +142,11 @@ class ObpBridgeSession:
"""Move to where DNS now says the peer is. True when it moved."""
host, port = str(addr[0]), int(addr[1])
self.dns_checked_at = at
if self.peer == (host, port):
return False
was = self.peer
if self.learned_peer and self.learned_peer[0] != host:
self.learned_peer = None # a port learned for the host it just left
self.resolved_peer = (host, port)
return True
return self.peer != was
def forget_learned_peer(self) -> None:
"""Drop what the wire taught us and fall back to the configured peer."""

@ -2413,7 +2413,10 @@ class HBPProtocol(DatagramProtocol):
_session.note_keepalive(_now)
# Anyone with the passphrase can send one, so it may bootstrap a peer
# we have no address for, never move one we have. DMRD/DMRE do that.
if not _session.peer_known:
if _session.dns_anchored:
# accepts_source vetted the host above, so this only refines the port.
_session.learn_peer(_sockaddr, at=_now)
elif not _session.peer_known:
if _session.learn_peer(_sockaddr, at=_now):
logger.info(
"(%s) *BridgeControl* OBP peer address learned from keepalive: %s:%s",

@ -69,6 +69,33 @@ def test_a_dns_anchored_peer_ignores_what_the_wire_says() -> None:
assert session.peer == _CONFIGURED
def test_a_name_that_never_resolved_anchors_nothing() -> None:
"""normalize_obp_config leaves TARGET_SOCK as (None, port) when the name does not
resolve at startup. Anchoring on that would take the link off the air for good."""
session = ObpBridgeSession(
system_name="OBP-FR", configured_peer=(None, 62201), dns_host="peer.example.net"
)
assert session.dns_anchored is False
assert session.learn_peer(_ELSEWHERE, at=_NOW) is True
def test_a_dns_anchored_peer_takes_a_new_port_on_its_own_host() -> None:
"""The name pins the host. A peer answers from whatever socket it bound, which
NAT may rewrite and which needs not be the port we send to."""
session = _dns_session()
other_port = (_CONFIGURED[0], 57933)
assert session.learn_peer(other_port, at=_NOW) is True
assert session.peer == other_port
def test_resolving_elsewhere_drops_a_port_learned_on_the_old_host() -> None:
session = _dns_session()
session.learn_peer((_CONFIGURED[0], 57933), at=_NOW)
assert session.adopt_resolved(_ELSEWHERE, at=_NOW) is True
assert session.peer == _ELSEWHERE
assert session.learned_peer is None
def test_a_dns_anchored_peer_moves_when_the_name_resolves_elsewhere() -> None:
session = _dns_session()
assert session.adopt_resolved(_ELSEWHERE, at=_NOW) is True

@ -96,8 +96,11 @@
{"case":{"desc":"from 9.9.9.9:62201 relax=True","from":["9.9.9.9",62201],"kind":"bcst","name":"bcst from 9.9.9.9:62201 relax=True","relax":true,"stream":1358954592},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[5,"(OBP-FR) *BridgeControl* BCST STUN request received"],[20,"(OBP-FR) Bridge STUNned, discarding"]],"quenched":[]}}
{"case":{"desc":"with no TARGET_IP configured","from":["82.65.127.86",62201],"kind":"bcka","name":"bcka with no TARGET_IP configured","no_peer":true,"stream":1358954593},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: , TARGET_PORT: 62201"],[10,"(OBP-FR) *BridgeControl* not sending KeepAlive, TARGET not currently known"],[20,"(OBP-FR) *BridgeControl* OBP peer address learned from keepalive: 82.65.127.86:62201"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from the resolved address","dns_host":"peer.example.net","from":["82.65.127.86",62201],"kind":"v1","name":"v1 dns-anchored, from the resolved address","relax":true,"stream":1358954594},"effects":{"delivered":[[2130003,214]],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from elsewhere","dns_host":"peer.example.net","from":["9.9.9.9",62201],"kind":"v1","name":"v1 dns-anchored, from elsewhere","relax":true,"stream":1358954595},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[20,"(OBP-FR) *BridgeControl* DMRD from 9.9.9.9:62201 discarded: peer.example.net resolves to 82.65.127.86:62201"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from the resolved address","dns_host":"peer.example.net","from":["82.65.127.86",62201],"kind":"bcka","name":"bcka dns-anchored, from the resolved address","relax":true,"stream":1358954596},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from elsewhere","dns_host":"peer.example.net","from":["9.9.9.9",62201],"kind":"bcka","name":"bcka dns-anchored, from elsewhere","relax":true,"stream":1358954597},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[20,"(OBP-FR) *BridgeControl* BCKA from 9.9.9.9:62201 discarded: peer.example.net resolves to 82.65.127.86:62201"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from the resolved address","dns_host":"peer.example.net","from":["82.65.127.86",62201],"kind":"bcsq","name":"bcsq dns-anchored, from the resolved address","relax":true,"stream":1358954598},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[20,"(OBP-FR) *BridgeControl* BCSQ accepted: stream_id=1358954598 TGID=214 (peer quenched; forwarding on this OBP stops for this stream/TG)"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from elsewhere","dns_host":"peer.example.net","from":["9.9.9.9",62201],"kind":"bcsq","name":"bcsq dns-anchored, from elsewhere","relax":true,"stream":1358954599},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[20,"(OBP-FR) *BridgeControl* BCSQ from 9.9.9.9:62201 discarded: peer.example.net resolves to 82.65.127.86:62201"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from the resolved host on another port","dns_host":"peer.example.net","from":["82.65.127.86",57933],"kind":"v1","name":"v1 dns-anchored, from the resolved host on another port","relax":true,"stream":1358954595},"effects":{"delivered":[[2130003,214]],"egress":[[24,["82.65.127.86",57933]],[73,["82.65.127.86",57933]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[10,"(OBP-FR) *BridgeControl* OBP peer address sync to 82.65.127.86:57933 (RELAX_CHECKS; was 82.65.127.86:62201)"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from elsewhere","dns_host":"peer.example.net","from":["9.9.9.9",62201],"kind":"v1","name":"v1 dns-anchored, from elsewhere","relax":true,"stream":1358954596},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[20,"(OBP-FR) *BridgeControl* DMRD from 9.9.9.9:62201 discarded: peer.example.net resolves to 82.65.127.86:62201"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from the resolved address","dns_host":"peer.example.net","from":["82.65.127.86",62201],"kind":"bcka","name":"bcka dns-anchored, from the resolved address","relax":true,"stream":1358954597},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from the resolved host on another port","dns_host":"peer.example.net","from":["82.65.127.86",57933],"kind":"bcka","name":"bcka dns-anchored, from the resolved host on another port","relax":true,"stream":1358954598},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",57933]],[73,["82.65.127.86",57933]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from elsewhere","dns_host":"peer.example.net","from":["9.9.9.9",62201],"kind":"bcka","name":"bcka dns-anchored, from elsewhere","relax":true,"stream":1358954599},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[20,"(OBP-FR) *BridgeControl* BCKA from 9.9.9.9:62201 discarded: peer.example.net resolves to 82.65.127.86:62201"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from the resolved address","dns_host":"peer.example.net","from":["82.65.127.86",62201],"kind":"bcsq","name":"bcsq dns-anchored, from the resolved address","relax":true,"stream":1358954600},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[20,"(OBP-FR) *BridgeControl* BCSQ accepted: stream_id=1358954600 TGID=214 (peer quenched; forwarding on this OBP stops for this stream/TG)"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from the resolved host on another port","dns_host":"peer.example.net","from":["82.65.127.86",57933],"kind":"bcsq","name":"bcsq dns-anchored, from the resolved host on another port","relax":true,"stream":1358954601},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[20,"(OBP-FR) *BridgeControl* BCSQ accepted: stream_id=1358954601 TGID=214 (peer quenched; forwarding on this OBP stops for this stream/TG)"]],"quenched":[]}}
{"case":{"desc":"dns-anchored, from elsewhere","dns_host":"peer.example.net","from":["9.9.9.9",62201],"kind":"bcsq","name":"bcsq dns-anchored, from elsewhere","relax":true,"stream":1358954602},"effects":{"delivered":[],"egress":[[24,["82.65.127.86",62201]],[73,["82.65.127.86",62201]]],"log":[[20,"(OBP-FR) Starting OBP. TARGET_IP: 82.65.127.86, TARGET_PORT: 62201"],[20,"(OBP-FR) *BridgeControl* BCSQ from 9.9.9.9:62201 discarded: peer.example.net resolves to 82.65.127.86:62201"]],"quenched":[]}}

@ -286,7 +286,11 @@ def _cases() -> list[dict[str, Any]]:
# TARGET_IP written as a name: only what it resolves to is this peer, however
# RELAX_CHECKS is set, because a name is an identity and an address is not.
for kind in ("v1", "bcka", "bcsq"):
for addr, where in ((PEER, "the resolved address"), (("9.9.9.9", 62201), "elsewhere")):
for addr, where in (
(PEER, "the resolved address"),
((PEER[0], 57933), "the resolved host on another port"),
(("9.9.9.9", 62201), "elsewhere"),
):
add(
kind=kind,
desc=f"dns-anchored, from {where}",

@ -66,14 +66,15 @@ def test_ingress_effects_match_the_recording(case: dict, recorded: dict[str, dic
[c for c in CASES if c["kind"] == "bcka" and not c.get("no_peer")],
ids=lambda c: c["name"],
)
def test_a_keepalive_never_moves_egress_off_the_peer(case: dict) -> None:
def test_a_keepalive_never_moves_egress_to_another_host(case: dict) -> None:
"""A keepalive carries no NETWORK_ID, so anyone holding the passphrase can send
one: a second instance of the peer, or another bridge on a shared-passphrase
mesh. It must not decide where this bridge transmits. Recorded above as well,
but asserted here so regenerating the corpus cannot drop it.
mesh. It may refine the port on the host we already talk to, never move the
bridge to a different host. Recorded above as well, but asserted here so
regenerating the corpus cannot drop it.
"""
for _size, addr in observe(case)["egress"]:
assert tuple(addr) == PEER
assert tuple(addr)[0] == PEER[0]
def test_a_keepalive_bootstraps_a_bridge_with_no_configured_peer() -> None:

Loading…
Cancel
Save

Powered by TurnKey Linux.