Skip to content

Latest commit

 

History

History
115 lines (98 loc) · 6.13 KB

File metadata and controls

115 lines (98 loc) · 6.13 KB

dns

DNS resolver: RFC 1035 message codec + UDP/TCP/DoH transports, forward (A/AAAA) and reverse (PTR) lookups.

  • The /etc/hosts + UDP-PTR core, the general codec, TCP, EDNS(0) and DoH fill a real std gap (std.Io.net.HostName.lookup is forward-only and opaque — no PTR, no server/transport control, no DoH).
  • Model after: Go net dnsclient + miekg/dns (message codec) / c-ares; RFC 1035 (wire format), RFC 8484 (DoH).
  • Deps: netaddr (address parse/format, reverse-name construction, RFC 6724 result ordering), http (DoH transport), std.json (DoH-JSON), std.Io.net (UDP/TCP).

Provenance: clean-room from RFC 1035 (wire), RFC 8484 (DoH), RFC 2782 (SRV) and RFC 8659 (CAA) throughout, including the /etc/hosts + UDP-PTR core; design references miekg/dns (BSD-3-Clause) and c-ares (MIT) — behavior only, no source copied. Recorded, together with the live-capture reasoning for src/goldens.zig, in NOTICE beside this file. No fping code is involved — fping has no resolver at all, and an earlier note claiming derivation was a bookkeeping error.

Layout

File Role
src/message.zig Pure wire codec: encode query / decode response — header, question, RR sections, name-compression pointers (strictly-backwards rule + jump budget: loops impossible), A/AAAA/PTR/CNAME/NS/MX/TXT/SOA, EDNS(0) OPT. No I/O; golden-byte tested; fuzzed.
src/config.zig /etc/resolv.conf + /etc/hosts parsing, Go-nameList search expansion. Pure string logic, fixture-tested.
src/Resolver.zig Blocking client: UDP (TC bit → TCP retry), TCP (2-byte length prefix), DoH POST/GET (application/dns-message), DoH-JSON (application/dns-json).
src/root.zig Vocabulary re-exports + netaddr bridges (reverseName, recordIp).
src/testdata/reply_*.bin Three replies captured off a real loopback socket by tools/interop.zig: one lying about the question, one with no question section, one honest. Replayed hermetically by Resolver.zig.
tools/interop.zig The live half of that anchor: a hostile UDP server on 127.0.0.1 answering a real query() from a real second thread. zig build interop-dns (no peer, no network); -- --capture re-takes the frames. Never compiled into test-dns.

Usage

const dns = @import("dns");

var resolver = dns.Resolver.init(io, gpa, .{});
defer resolver.deinit();

// getaddrinfo-like: /etc/hosts first, then A + AAAA with the search list,
// RFC 6724-ordered on Linux.
const ips = try resolver.lookupIp("example.com");
defer gpa.free(ips);

// Any record type; caller inspects rcode/answers, message owns its memory.
var msg = try resolver.resolve("example.com", .mx);
defer msg.deinit();
for (msg.answers) |rec| switch (rec.data) {
    .mx => |mx| std.debug.print("{d} {s}\n", .{ mx.preference, mx.exchange }),
    else => {},
};

// Reverse (PTR): hosts file first, then in-addr.arpa / ip6.arpa via netaddr.
const names = try resolver.reverse(netaddr.parseIp("8.8.8.8").?);
defer resolver.freeNames(names);

// DNS-over-HTTPS (RFC 8484) — same API, different transport:
var doh = dns.Resolver.init(io, gpa, .{ .doh_url = "https://dns.google/dns-query" });
defer doh.deinit();
var m2 = try doh.query("example.com", .aaaa);
defer m2.deinit();

// Low-level codec, transport-agnostic — a codec-only consumer never needs
// to name `dns.message` (which would pull in `Resolver`, and with it `http`):
var buf: [dns.max_query_len]u8 = undefined;
const packet = try dns.encodeQuery(&buf, "example.com", .a, .{ .id = 1 });
var decoded = try dns.decode(gpa, response_bytes);
defer decoded.deinit();

Behavior notes

  • Decoded names are dotted text without the trailing root dot (root = ""); no \DDD escape handling — labels are raw bytes. A consumer that needs the true label boundaries (not text dot-counting) reads Record.labels (audit F7) — slices into Record.name, populated for every decoded record; empty on a hand-built Record that never set it.
  • resolve returns the last response even on NXDOMAIN/empty — inspect Message.rcode(); lookupIp returns an empty slice when nothing resolves.
  • EDNS(0) advertises a 1232-byte UDP payload by default (DNS flag day 2020); set edns_udp_size = null for plain RFC 1035 queries.
  • DoH uses query id 0 (RFC 8484 §4.1 cache friendliness).
  • A reply is accepted only if it comes from the server queried, carries the (per-datagram fresh) transaction id, has QR set and echoes exactly our question — name (case-insensitive), type, class IN. lookupIp/reverse additionally return only records owned by the queried name or by a CNAME target chained from it in the same answer; resolve/query hand back the whole message for the caller to judge.
  • Compression pointers must point strictly backwards (Go dnsmessage rule); combined with the 253-char name cap and a 16-jump budget, adversarial pointer loops always fail fast with an error — the fuzz test starts from the six live captures and hammers this.
  • timeout_ms bounds each UDP attempt and each TCP attempt end to end (connect + write + reads, via a canceled task — std 0.16.0 has no stream read deadline). It is per attempt per server: a query may take timeout_ms × attempts × servers, and lookupIp runs one per search candidate and address family on top. Set it small, shorten servers, or pass a rooted name (trailing dot) when a call must be bounded.
  • DoH-JSON (queryJson) validates name like the wire path and percent-encodes it into the URL.

Tests

zig build test-dns — offline: golden query bytes, canned responses (compression, CNAME chain, MX/TXT/SOA/OPT, PTR), adversarial packets (truncations at every offset, pointer loops, bad rdata lengths, hostile counts under a memory limit), fuzzed decode seeded with the live captures, resolv.conf/hosts fixtures, search-list order, reverse-name goldens (incl. the RFC 3596 example), response correlation, the bailiwick rule, DoH-JSON URL encoding, and loopback stubs (a lying UDP server, a silent TCP server). Live tests (UDP, TCP, DoH POST/GET, DoH-JSON, PTR of 8.8.8.8) run against public resolvers when the network is up and skip via error.SkipZigTest when it is not.