-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. --[==[ Small shared helpers used by both dns.client and dns.iter: numeric/string qtype resolution, query-message building (RD and DO bits parameterized), response validation, and SOA extraction. Kept here so the two resolvers can't drift on wire-format details. ]==] local types = require("dns.types") local dns_name = require("dns.name") local TYPE = types.TYPE local upper = string.upper local ipairs = ipairs local type = type local tonumber = tonumber ---! Resolve a qtype to a numeric code ---@ resolve_qtype(`qtype`{.any}) -> `code`{.num .or_nil}, `err`{.str .err} --[===[ Accepts a numeric code directly, a known mnemonic (`"A"`, `"AAAA"`, case insensitive), or a numeric string (`"48"`). Returns `nil, err` for unknown strings or non-string/number inputs. ]===] local resolve_qtype = function(qtype) if type(qtype) == "number" then return qtype end if type(qtype) == "string" then local num = TYPE[upper(qtype)] if num then return num end num = tonumber(qtype) if num then return num end return nil, "unknown query type: " .. qtype end return nil, "qtype must be a string or number" end ---! Build a DNS query message table (not wire-encoded) ---@ build_query(`qname`{.str}, `qtype`{.num}, `id`{.num}, `opts`{.tbl .opt}) -> `msg`{.tbl} --[===[ Returns a message table ready to pass to `message.encode()`. Options: - `edns_buffer_size` — when set, attach an EDNS OPT RR with this UDP payload size. Omit (or pass nil/false) to build a bare RFC1035 query. - `rd` — Recursion Desired flag (default `true` for recursive resolvers; set `false` for iterative queries). - `do_bit` — EDNS DNSSEC OK bit. Only meaningful when `edns_buffer_size` is set (default `false`). ]===] local build_query = function(qname, qtype, id, opts) opts = opts or {} local rd = opts.rd if rd == nil then rd = true end local msg = { header = { id = id, qr = 0, opcode = 0, rd = rd, rcode = 0, }, question = { { name = qname, qtype = qtype, qclass = 1 } }, answer = {}, authority = {}, additional = {}, } if opts.edns_buffer_size then msg.edns = { udp_size = opts.edns_buffer_size, extended_rcode = 0, version = 0, do_bit = opts.do_bit or false, options = {}, } end return msg end ---! Validate a decoded DNS response against the original query ---@ validate_response(`resp`{.tbl}, `id`{.num}, `qname`{.str}, `qtype`{.num}, `sent_0x20_name`{.str .opt}) -> `ok`{.bool .or_nil}, `err`{.str .err} --[===[ Checks that the response matches the query we sent: ID match, a question section with matching qtype and name. If `sent_0x20_name` is non-nil, the response name is verified against the 0x20-encoded original (case-preserving); otherwise a case-insensitive name comparison is used. ]===] local validate_response = function(resp, id, qname, qtype, sent_0x20_name) if resp.header.id ~= id then return nil, "id mismatch" end if not resp.question or #resp.question < 1 then return nil, "missing question section" end local rq = resp.question[1] if rq.qtype ~= qtype then return nil, "qtype mismatch" end if sent_0x20_name then if not dns_name.verify_0x20(sent_0x20_name, rq.name) then return nil, "0x20 name mismatch" end else if not dns_name.equal(rq.name, qname) then return nil, "question name mismatch" end end return true end ---! Extract the first SOA record from an authority section ---@ extract_soa(`authority`{.tbl .or_nil}) -> `soa`{.tbl .or_nil} local extract_soa = function(authority) if not authority then return nil end for _, rr in ipairs(authority) do if rr.type == TYPE.SOA then return rr end end return nil end return { resolve_qtype = resolve_qtype, build_query = build_query, validate_response = validate_response, extract_soa = extract_soa, }