"""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")]