hbc, hbc_mini.py and hbc_mini.c resolved the server once at startup, so an address change needed a client restart. They now re-resolve every TTL seconds (smallest A/AAAA TTL, floor 30s, 300s when unknown) and re-point, add or drop connections to follow the new addresses; a failed lookup keeps the current ones. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
120 lines
4.0 KiB
Python
120 lines
4.0 KiB
Python
"""TTL-driven re-resolution of the server hostnames in the full client."""
|
|
import socket
|
|
import struct
|
|
|
|
from hbd.client import dnsttl
|
|
|
|
|
|
def _rr(rtype, ttl, rdata):
|
|
return b"\xc0\x0c" + struct.pack("!HHIH", rtype, 1, ttl, len(rdata)) + rdata
|
|
|
|
|
|
def _response(qid, qtype, answers, rcode=0):
|
|
q = dnsttl.build_query("hbd.example.com", qtype, qid)[12:]
|
|
return struct.pack("!HHHHHH", qid, 0x8180 | rcode, 1, len(answers), 0, 0) + q + b"".join(answers)
|
|
|
|
|
|
def test_min_ttl_covers_cname_and_address():
|
|
resp = _response(7, dnsttl.TYPE_A, [_rr(dnsttl.TYPE_CNAME, 60, b"\x01x\x00"),
|
|
_rr(dnsttl.TYPE_A, 300, b"\x01\x02\x03\x04")])
|
|
assert dnsttl.min_answer_ttl(resp, 7, dnsttl.TYPE_A) == 60
|
|
|
|
|
|
def test_no_answer_of_requested_type_is_unknown():
|
|
assert dnsttl.min_answer_ttl(_response(7, dnsttl.TYPE_AAAA, []), 7, dnsttl.TYPE_AAAA) is None
|
|
|
|
|
|
def test_error_rcode_and_foreign_id_are_unknown():
|
|
ans = [_rr(dnsttl.TYPE_A, 300, b"\x01\x02\x03\x04")]
|
|
assert dnsttl.min_answer_ttl(_response(7, 1, ans, rcode=3), 7, 1) is None
|
|
assert dnsttl.min_answer_ttl(_response(7, 1, ans), 8, 1) is None
|
|
|
|
|
|
def test_truncated_response_is_unknown():
|
|
assert dnsttl.min_answer_ttl(b"\x00\x07\x81", 7, 1) is None
|
|
|
|
|
|
def test_refresh_delay():
|
|
assert dnsttl.refresh_delay([300, 120, None]) == 120
|
|
assert dnsttl.refresh_delay([0]) == dnsttl.MIN_TTL
|
|
assert dnsttl.refresh_delay([None]) == dnsttl.DEFAULT_TTL
|
|
|
|
|
|
def test_ip_literal_has_no_ttl():
|
|
assert dnsttl.lookup_ttl("192.0.2.1") is None
|
|
assert dnsttl.lookup_ttl("2001:db8::1") is None
|
|
|
|
|
|
class _C:
|
|
def __init__(self, addr, af=socket.AF_INET):
|
|
self.addr, self.af = addr, af
|
|
|
|
def set_target(self, addr, af):
|
|
self.addr, self.af = addr, af
|
|
|
|
|
|
V4, V6 = socket.AF_INET, socket.AF_INET6
|
|
|
|
|
|
def test_retarget_follows_address_change_in_place():
|
|
first, second = _C("1.1.1.1"), _C("2001:db8::1", V6)
|
|
new, gone = dnsttl.retarget([first, second], [(V4, "9.9.9.9"), (V6, "2001:db8::9")])
|
|
assert (new, gone) == ([], [])
|
|
assert (first.addr, second.addr) == ("9.9.9.9", "2001:db8::9")
|
|
|
|
|
|
def test_retarget_unchanged_is_a_noop():
|
|
conns = [_C("1.1.1.1")]
|
|
assert dnsttl.retarget(conns, [(V4, "1.1.1.1")]) == ([], [])
|
|
|
|
|
|
def test_retarget_reports_surplus_new_and_gone():
|
|
a, b = _C("1.1.1.1"), _C("2.2.2.2")
|
|
new, gone = dnsttl.retarget([a, b], [(V4, "3.3.3.3")])
|
|
assert new == [] and gone == [b] and a.addr == "3.3.3.3"
|
|
new, gone = dnsttl.retarget([a], [(V4, "3.3.3.3"), (V4, "4.4.4.4")])
|
|
assert new == [(V4, "4.4.4.4")] and gone == []
|
|
|
|
|
|
def test_retarget_never_drops_the_first_connection():
|
|
a, b = _C("1.1.1.1"), _C("2.2.2.2")
|
|
new, gone = dnsttl.retarget([a, b], [(V4, "2.2.2.2")])
|
|
assert new == [] and gone == [b] and a.addr == "2.2.2.2"
|
|
|
|
|
|
def test_resolve_failure_returns_none(monkeypatch):
|
|
def boom(*a):
|
|
raise socket.gaierror("nope")
|
|
monkeypatch.setattr(socket, "getaddrinfo", boom)
|
|
assert dnsttl.resolve(["x.invalid"], 1, 0) is None
|
|
|
|
|
|
def test_refresher_follows_a_change_and_adds_a_family(monkeypatch):
|
|
import asyncio
|
|
from hbd.client import main as hbc
|
|
|
|
answers = [[(V4, "9.9.9.9"), (V6, "2001:db8::9")], None, [(V4, "9.9.9.9")]]
|
|
monkeypatch.setattr(dnsttl, "resolve", lambda *a: answers.pop(0) if len(answers) > 1 else answers[0])
|
|
monkeypatch.setattr(dnsttl, "lookup_ttl", lambda *a: None)
|
|
monkeypatch.setattr(dnsttl, "DEFAULT_TTL", 0.01)
|
|
monkeypatch.setattr(dnsttl, "FAILED_RETRY", 0.01)
|
|
|
|
async def idle(*a):
|
|
await asyncio.sleep(10)
|
|
monkeypatch.setattr(hbc, "heartbeat_sender", idle)
|
|
|
|
async def go():
|
|
hbc.shutdown_event = asyncio.Event()
|
|
hbc.active_tasks = []
|
|
conns = [hbc.AsyncConnection(1, "1.1.1.1", 5, V4, "me")]
|
|
task = asyncio.create_task(hbc.dns_refresher(conns, ["h"], 5, 0, "me", 10, None, 2))
|
|
await asyncio.sleep(0.2)
|
|
hbc.shutdown_event.set()
|
|
await task
|
|
for t in hbc.active_tasks:
|
|
t.cancel()
|
|
return conns
|
|
|
|
conns = asyncio.run(go())
|
|
assert [(c.conn_id, c.addr) for c in conns] == [(1, "9.9.9.9")]
|