"""DNS monitors that name a resolver ask it directly (#5214).""" import asyncio import struct import pytest from steward.monitors import dns def _reply(qid, name, records, rcode=0): """A reply to build_query(qid, name, A): the question, then `records` as (type, rdata), each owner name a pointer back to the question's.""" question = dns.build_query(qid, name, 1)[12:] head = struct.pack("!HHHHHH", qid, 0x8180 | rcode, 1, len(records), 0, 0) body = b"".join(b"\xc0\x0c" + struct.pack("!HHIH", rtype, 1, 60, len(rdata)) + rdata for rtype, rdata in records) return head + question + body def test_the_query_asks_one_recursive_question(): q = dns.build_query(0x1234, "google.com.", 28) assert q[:12] == struct.pack("!HHHHHH", 0x1234, 0x0100, 1, 0, 0, 0) assert q[12:] == b"\x06google\x03com\x00" + struct.pack("!HH", 28, 1) def test_an_answer_yields_its_addresses_past_a_cname(): cname = b"\x03www\xc0\x0c" data = _reply(7, "example.com", [(5, cname), (1, bytes([93, 184, 216, 34])), (1, bytes([10, 0, 0, 1]))]) assert dns.parse_answer(data, 1) == ["93.184.216.34", "10.0.0.1"] assert dns.parse_answer(data, 28) == [] def test_a_server_error_is_named(): with pytest.raises(ValueError, match="NXDOMAIN"): dns.parse_answer(_reply(7, "nope.example", [], rcode=3), 1) def _fake_ask(answers): async def ask(name, resolver, qtype): got = answers[qtype] if isinstance(got, Exception): raise got return got return ask def test_a_resolver_check_is_up_and_timed(monkeypatch): monkeypatch.setattr(dns, "_ask", _fake_ask({1: ["142.250.80.46"], 28: ["2607:f8b0::200e"]})) res = asyncio.run(dns.dns_check("google.com", resolver="1.1.1.1")) assert res["is_up"] and res["resolved_ip"] == "142.250.80.46" assert isinstance(res["response_ms"], float) def test_a_resolver_that_says_no_such_name_is_down_with_why(monkeypatch): monkeypatch.setattr(dns, "_ask", _fake_ask({1: ValueError("NXDOMAIN"), 28: ValueError("NXDOMAIN")})) res = asyncio.run(dns.dns_check("nope.example", resolver="192.168.0.9")) assert not res["is_up"] and res["error_msg"] == "192.168.0.9: NXDOMAIN" def test_a_silent_resolver_records_no_time(monkeypatch): silent = TimeoutError("no answer within 5s") monkeypatch.setattr(dns, "_ask", _fake_ask({1: silent, 28: silent})) res = asyncio.run(dns.dns_check("google.com", resolver="10.9.9.9")) assert not res["is_up"] and "response_ms" not in res assert res["error_msg"] == "10.9.9.9: no answer within 5s" def test_no_resolver_keeps_the_containers_lookup(monkeypatch): called = {} async def resolve(address): called["address"] = address return {"resolved": True, "ip": "10.0.0.2", "all_ips": ["10.0.0.2"]} monkeypatch.setattr(dns, "_resolve", resolve) monkeypatch.setattr(dns, "_ask", _fake_ask({})) # would KeyError if reached res = asyncio.run(dns.dns_check("nas.lan")) assert called == {"address": "nas.lan"} and res["is_up"] and "response_ms" not in res