feat(client): re-resolve the server hostnames at the DNS TTL
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>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""TTL-driven re-resolution in the standalone scripts/hbc_mini.py."""
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
_PATH = pathlib.Path(__file__).resolve().parents[1] / "scripts" / "hbc_mini.py"
|
||||
_SPEC = importlib.util.spec_from_file_location("hbc_mini", _PATH)
|
||||
mini = importlib.util.module_from_spec(_SPEC)
|
||||
sys.modules["hbc_mini"] = mini
|
||||
_SPEC.loader.exec_module(mini)
|
||||
|
||||
V4, V6 = socket.AF_INET, socket.AF_INET6
|
||||
|
||||
|
||||
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 = mini._dns_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, 1, [_rr(5, 60, b"\x01x\x00"), _rr(1, 300, b"\x01\x02\x03\x04")])
|
||||
assert mini._dns_min_answer_ttl(resp, 7, 1) == 60
|
||||
|
||||
|
||||
def test_unknown_ttl_cases():
|
||||
assert mini._dns_min_answer_ttl(_response(7, 28, []), 7, 28) is None
|
||||
assert mini._dns_min_answer_ttl(_response(7, 1, [_rr(1, 9, b"1234")], rcode=3), 7, 1) is None
|
||||
assert mini._dns_min_answer_ttl(b"\x00\x07\x81", 7, 1) is None
|
||||
assert mini._dns_lookup_ttl("192.0.2.1") is None
|
||||
|
||||
|
||||
def test_refresh_delay():
|
||||
assert mini._dns_refresh_delay([300, 120, None]) == 120
|
||||
assert mini._dns_refresh_delay([0]) == mini._DNS_MIN_TTL
|
||||
assert mini._dns_refresh_delay([None]) == mini._DNS_DEFAULT_TTL
|
||||
|
||||
|
||||
def test_refresher_follows_a_change_and_adds_a_family(monkeypatch):
|
||||
answers = [[(V4, "9.9.9.9"), (V6, "2001:db8::9")], None, [(V4, "9.9.9.9")]]
|
||||
monkeypatch.setattr(mini, "_dns_resolve", lambda *a: answers.pop(0) if len(answers) > 1 else answers[0])
|
||||
monkeypatch.setattr(mini, "_dns_lookup_ttl", lambda *a: None)
|
||||
monkeypatch.setattr(mini, "_DNS_DEFAULT_TTL", 0.01)
|
||||
monkeypatch.setattr(mini, "_DNS_FAILED_RETRY", 0.01)
|
||||
|
||||
async def idle(*a):
|
||||
await asyncio.sleep(10)
|
||||
monkeypatch.setattr(mini, "_heartbeat_sender", idle)
|
||||
|
||||
async def go():
|
||||
mini._running = True
|
||||
mini._shutdown_event = asyncio.Event()
|
||||
mini._active_tasks = []
|
||||
conns = [mini.AsyncConnection(1, "1.1.1.1", 5, V4, "me")]
|
||||
task = asyncio.create_task(mini._dns_refresher(conns, ["h"], 5, 0, "me", 10, None, 2))
|
||||
await asyncio.sleep(0.2)
|
||||
mini._running = False
|
||||
mini._shutdown_event.set()
|
||||
await task
|
||||
for t in mini._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")]
|
||||
assert conns[0].af == V4
|
||||
Reference in New Issue
Block a user