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>
73 lines
2.6 KiB
Python
73 lines
2.6 KiB
Python
"""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
|