DNS message encoding and decoding for Zig, with no I/O and no allocation.
  • Zig 94.7%
  • Python 3.1%
  • Nix 2.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie 395e454179
All checks were successful
test / test (push) Successful in 8m20s
test / docs (push) Successful in 6m28s
Update zon2nix and regenerate build.zig.zon.nix
zon2nix no longer depends on the zig-overlay, so the overlay leaves
flake.lock entirely. The regenerated expression drops the comment saying
nixpkgs lacks Zig 0.17, builds the package farm with runCommand so it can
be substituted from a cache, and tolerates an empty dependency set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUmrsokV13zXmHVrDJaBSc
2026-10-08 22:57:31 -05:00
.forgejo/workflows Let the caller choose what a compression pointer may point at 2026-09-13 17:25:25 -05:00
LICENSES Decode and encode DNS messages 2026-09-13 16:05:51 -05:00
src Build with the Zig 0.17.0 release 2026-10-02 19:23:06 -05:00
tests Build with the Zig 0.17.0 release 2026-10-02 19:23:06 -05:00
tools Build with the Zig 0.17.0 release 2026-10-02 19:23:06 -05:00
.gitignore Decode and encode DNS messages 2026-09-13 16:05:51 -05:00
build.zig Build with the Zig 0.17.0 release 2026-10-02 19:23:06 -05:00
build.zig.zon Release 0.2.0 2026-10-02 19:26:18 -05:00
build.zig.zon.nix Update zon2nix and regenerate build.zig.zon.nix 2026-10-08 22:57:31 -05:00
flake.lock Update zon2nix and regenerate build.zig.zon.nix 2026-10-08 22:57:31 -05:00
flake.nix Take Zig 0.17 from nixpkgs rather than the overlay 2026-10-08 22:28:21 -05:00
README.md Point Zig 0.16 users at v0.1.0 2026-10-02 19:27:24 -05:00
REUSE.toml Let the caller choose what a compression pointer may point at 2026-09-13 17:25:25 -05:00

zig-dns

DNS messages for Zig 0.17, decoded and encoded, and nothing else.

This library turns bytes into a message you can read and a message you describe into bytes. It opens no sockets, reads no files, starts no threads, consults no configuration and holds no cache — and it never allocates. What you do with a message is somewhere else, which is the point: the hard parts of this format are the same whether the bytes came from a socket, from a packet capture, or from a fuzzer.

The API documentation is generated from the doc comments, which are most of the explanation of why DNS is the shape it is.

Where this lives

The repository lives in three places that carry the same history. The Forgejo instance at https://git.jcollie.dev/jeff/zig-dns is the web-visible one:

$ git clone https://git.jcollie.dev/jeff/zig-dns.git

it is mirrored on Tangled at https://tangled.org/jcollie.dev/zig-dns, and it is also on the Radicle network, where the repository's identifier is

rad:z2zFuqA4NbWoKuX8FoSnwGZ1n2XU3

and rad clone rad:z2zFuqA4NbWoKuX8FoSnwGZ1n2XU3 fetches it from any node that seeds it. Any of the three is the whole project.

Adding it to a project

$ zig fetch --save git+https://git.jcollie.dev/jeff/zig-dns.git

and then, in build.zig:

const dns = b.dependency("dns", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("dns", dns.module("dns"));

main needs Zig 0.17.0. For Zig 0.16.0, use the v0.1.0 tag, the last release of zig-dns to build with it:

$ zig fetch --save git+https://git.jcollie.dev/jeff/zig-dns.git#v0.1.0
$ git clone -b v0.1.0 https://git.jcollie.dev/jeff/zig-dns.git

The zig-0.16 branch points at the same commit. It is kept for 0.16 users rather than developed; new work lands on main only.

Quick start

Reading:

const dns = @import("dns");

const message = try dns.Message.parse(bytes);
std.debug.print("{f}\n", .{message}); // prints it the way dig does

var it = message.answers();
while (try it.next()) |record| {
    switch (try record.data()) {
        .a => |address| std.debug.print("{f}\n", .{address}),
        .cname => |name| std.debug.print("{f}\n", .{name}),
        else => {},
    }
}

Writing:

var buf: [512]u8 = undefined;

var builder = try dns.Builder.init(&buf, .{
    .id = 0x1234,
    .recursion_desired = true,
}, .{});
try builder.question(.{
    .name = dns.Name.literal("example.com"),
    .type = .a,
});
try builder.opt(.{}); // EDNS(0), without which almost nothing works
const query = builder.finish();

Name.literal encodes and validates the name at compile time, so a typo is a compile error and it needs no buffer. For a name that is not known until run time there is Name.fromText, which writes into a Name.Buffer you supply:

var names: dns.Name.Buffer = undefined;
const name = try dns.Name.fromText(&names, whatever_the_user_typed);

What is in it

Message the four sections, with each one's offset found in a single validating pass
Header the twelve bytes, and every flag, readable on their own when the rest cannot be trusted
Name labels, compression pointers, case-insensitive comparison and hashing, canonical order, the presentation escaping, and reading one back — absolute, or relative to an origin
Record an owner, a type, a TTL, and data that is only decoded when asked for
rdata seventy-seven record types, and RFC 3597 opaque data for everything else
edns the opt pseudo-record: payload size, extended response codes, cookies, client subnet, extended errors
Builder encoding, with name compression and an atomic record that rolls back rather than half-filling a buffer
serial RFC 1982 arithmetic: comparing two numbers that wrap, which an soa serial does and < gets wrong at the one moment it matters
tsig the bytes a TSIG signature covers, written into a writer — which fields, in which order, unsigned again and with the identifier put back. The keyed hash that consumes them is somebody else's
sig0 the same for SIG(0): the record data with the signature omitted, then the message before the signature was added. The public key operation that consumes them is somebody else's too
update the nine shapes a dynamic update takes — which class, which TTL and which empty rdata says "delete this RRset" rather than "add a record with nothing in it" — as two unions with one variant per subsection of §2.4 and §2.5

Seventy-seven record types have a parser of their own — every type dnspython parses, less the five that are only ever questions:

addresses and names A AAAA NS CNAME DNAME PTR SOA MX SRV NAPTR NSAP-PTR MD MF MB MG MR
text TXT SPF HINFO RP MINFO NINFO AVC RESINFO WALLET X25 ISDN GPOS
service and policy SVCB HTTPS CAA URI KX LP AFSDB RT PX
DNSSEC DS CDS DLV TA DNSKEY CDNSKEY KEY RRSIG SIG NSEC NSEC3 NSEC3PARAM NXT
keys published for elsewhere SSHFP TLSA SMIMEA CERT IPSECKEY OPENPGPKEY DHCID HIP AMTRELAY
zone maintenance CSYNC ZONEMD DSYNC
pseudo-records OPT TSIG TKEY
identifiers and locators NID L32 L64 EUI48 EUI64 APL LOC WKS A6 NSAP NULL UNSPEC

Anything else decodes as unknown and re-encodes byte for byte, because RFC 3597 requires that a resolver carry a type nobody has told it about. The five QTYPEs — ANY, AXFR, IXFR, MAILA, MAILB — are refused instead: they name a question and there is no such record, so data says so rather than handing back bytes that cannot mean anything.

What it will not do

No cryptography, and neither tsig nor sig0 is an exception. RFC 8945 §4.3 and RFC 2931 §3.1 say which bytes a signature covers, in which order, with the message unsigned again first — and that is serialisation, which is what this library is for. So both write those bytes into an Io.Writer and stop: point the writer at an HMAC and you have a signer, point it at Writer.fixed and you have the digest input in your hand, which is the only way a signature bug is ever found. What the writer does with them is not a message library's business.

rdata.dnssec decodes every DNSSEC record type and Dnskey.keyTag does the arithmetic that links a signature to the key that made it, but nothing here verifies one, and Tsig is decoded without its MAC ever being checked. Builder refuses to compress names in anything signed so that what a validator is handed is what was signed.

No zone files. Everything has a format that writes the presentation form, because a decoded message that cannot be printed is hard to trust, and nothing here reads one back — with one exception, which is names. Name.fromText and Name.fromTextRelative exist because a name is what a program has the moment somebody types one, long before any zone file is involved: a resolver needs it for its search list and a zone file reader needs it for $ORIGIN, and neither should have to write the escape rules again. The rest of the presentation format — the record types, the directives, the grammar — is zig-dns-zone, which is built on this.

No I/O, and no resolver. No retry, no truncation-and-retry-over-TCP, no cache, no root hints, no happy eyeballs. Those need a policy and a clock and this needs neither.

Three things worth knowing

Everything borrows. A decoded Message is a view over the buffer it was read from, and so is every Name and every record in it. A name can be compressed, meaning its labels are not where it appears to be but somewhere earlier in the message — which is why a Name holds the whole message and an offset, and why Name.clone exists for when one has to outlive the buffer.

Running out of room is normal. A DNS server does not decide how big its answer will be, it discovers it. So Builder.record is atomic: on error.NoSpace the builder is exactly as it was, with no half-written record and no compression pointer aimed at bytes that were rolled back. Filling a response is a loop and a catch:

for (answers) |rr| builder.record(.answer, rr) catch |err| switch (err) {
    error.NoSpace => {
        builder.setTruncated();
        break;
    },
    else => return err,
};

Where the format is under-specified, you get a say. RFC 1035 says a compression pointer names "a prior occurrence of the same name" and leaves two readings of "prior" in use. By default a pointer need only point below itself; .{ .names = .{ .pointers = .descending } } requires it to point below every offset already followed, which is what dnspython asks and makes termination obvious rather than argued:

const message = try dns.Message.parseWithOptions(bytes, .{
    .names = .{ .pointers = .descending },
});

Both refuse every loop, and no compressor can tell them apart — one that only points at what it has already written makes the targets descend on its own — so the difference is reachable only by a message nothing generated. Which is exactly the kind a decoder needs an answer for.

Decoding is strict about framing and lenient about meaning. A message whose sections do not add up, a name that points forwards, a record whose length disagrees with its type — all refused. A record type nobody knows, a response code nobody has defined, a class that is not IN — all carried through unchanged, because a library that cannot hold what it does not understand cannot be a forwarder, a proxy, or a debugging tool.

Building

zig build test runs everything; zig build check compiles the parts no test runs. IPv4 and IPv6 addresses are parsed and formatted by z46, which is the only dependency.

$ zig build test --summary all
$ zig build docs && zig build docs-serve   # http://127.0.0.1:8000/

Under Nix, nix develop -c zig build test uses the toolchain in flake.nix rather than whatever is on the path.

Testing

Beyond the unit tests beside each file, tests/messages.zig holds messages captured verbatim from a real resolver — a signed DNSKEY set, an HTTPS record with four parameters, a denial of existence — and checks both that they decode and that they survive being built again. A round trip through code that agrees with itself proves nothing about whether it agrees with the rest of the internet.

Checking it against something else

Every test above is this library agreeing with itself, which cannot catch the decoder and the encoder being wrong the same way. zig build differential puts a few hundred thousand mutations of the captured messages to this library and to dnspython, and compares whether each accepted the message and what it found in it:

$ nix develop -c zig build differential
200000 messages from seed 1: both accept 33445, both refuse 165839, disagree 742
...
Every disagreement is one that is written down.

The disagreements that remain are strictness choices rather than defects, and each is recorded in tests/differential.py with the reason — this library refusing a record whose length does not match its type where dnspython keeps the bytes as opaque data; dnspython refusing record types this library carries through as unknown; the two of them reading RFC 1035's "prior occurrence" differently when a compression pointer is involved. A kind of disagreement that is not written down fails the run, so a change to what is accepted has to be looked at rather than absorbed.

nix flake check runs the same comparison as a build, under both pointer rules and with the Zig dependency coming from build.zig.zon.nix rather than from the network, so it needs nothing on the path. Regenerate that file with nix develop -c zon2nix --17 --nix=build.zig.zon.nix build.zig.zon whenever a dependency moves.

Fuzzing

tests/fuzz.zig holds the properties that must survive any input at all: that decoding terminates, stays inside its buffer, and round-trips whatever it claims to have understood. Zig's own fuzzer runs them with coverage feedback:

$ zig build fuzz --fuzz                               # until interrupted
$ zig build fuzz --fuzz=1M                            # a bounded run
$ zig build fuzz --fuzz -Dfuzz-filter=names           # one target

zig build fuzz-run is a loop of our own over the same targets, with a corpus in place of coverage feedback. It does a fixed amount of work from a seed that can be given again, which is what CI runs, and replays a single input:

$ zig build fuzz-run                                  # a minute of each target
$ zig build fuzz-run -- --seconds 300 --target names
$ zig build fuzz-run -- --input fuzz-findings/x.bin --target names

License

MIT. The project follows the REUSE standard; reuse lint checks it.

References cited

The documents this library is written against, in the RFC citation format so that a reference here matches one anywhere else. Each entry says what the document is and what in src/ is written from it. All of them are also filed in a Zotero collection called zig-dns, with the canonical text of each RFC attached, so a citation can be taken from there rather than composed.

  • [RFC822] Crocker, D., "STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES", STD 11, RFC 822, August 1982, https://www.rfc-editor.org/info/rfc822. The mail address form that one half of a px record maps to an X.400 one.
  • [RFC973] Mockapetris, P., "Domain system changes and observations", RFC 973, January 1986, https://www.rfc-editor.org/info/rfc973. What made md and mf obsolete in favor of mx. Both still decode, as the domain name they always were.
  • [RFC1035] Mockapetris, P., "Domain names - implementation and specification", STD 13, RFC 1035, November 1987, https://www.rfc-editor.org/info/rfc1035. The format itself: the header, the four sections, labels and compression pointers (§4.1.4), the character-string (§3.3), the 512-byte UDP limit and the two-byte TCP length prefix (§4.2), and the presentation escaping (§5.1). Its "a prior occurrence of the same name" is the sentence both pointer rules read differently.
  • [RFC1183] Everhart, C., Mamakos, L., Ullmann, R., and P. Mockapetris, Ed., "New DNS RR Definitions", RFC 1183, October 1990, https://www.rfc-editor.org/info/rfc1183. AFSDB, RP, X25, ISDN and RT.
  • [RFC1712] Farrell, C., Schulze, M., Pleitner, S., and D. Baldoni, "DNS Encoding of Geographical Location", RFC 1712, November 1994, https://www.rfc-editor.org/info/rfc1712. GPOS, three decimal strings, which LOC replaced with something a machine can compare.
  • [RFC1876] Davis, C., Vixie, P., Goodwin, T., and I. Dickinson, "A Means for Expressing Location Information in the Domain Name System", RFC 1876, January 1996, https://www.rfc-editor.org/info/rfc1876. LOC, its base-and-exponent sizes, and §2's instruction to treat a version this does not know as data of unknown meaning.
  • [RFC1982] Elz, R. and R. Bush, "Serial Number Arithmetic", RFC 1982, August 1996, https://www.rfc-editor.org/info/rfc1982. How an soa serial is compared, which is not how a number is: serial is §3.2's comparison, including the pair of serials half the space apart that §3.2 leaves deliberately unordered and that this reports as null rather than deciding.
  • [RFC1996] Vixie, P., "A Mechanism for Prompt Notification of Zone Changes (DNS NOTIFY)", RFC 1996, August 1996, https://www.rfc-editor.org/info/rfc1996. The notify opcode.
  • [RFC2136] Vixie, P., Ed., Thomson, S., Rekhter, Y., and J. Bound, "Dynamic Updates in the Domain Name System (DNS UPDATE)", RFC 2136, April 1997, https://www.rfc-editor.org/info/rfc2136. The update opcode, which renames the four sections rather than changing them, and update entire: §2.4 and §2.5's nine shapes, which are encodings rather than ideas — the class, the TTL and the emptiness of the rdata are what tell a deletion from an addition — and §3.4.1's prescan, which a requestor can run on its own changes before spending a round trip learning notzone. Decoding one stays the caller's: a message library that reinterpreted the sections by opcode would be deciding something, which is one of the recorded disagreements with dnspython.
  • [RFC2163] Allocchio, C., "Using the Internet DNS to Distribute MIXER Conformant Global Address Mapping (MCGAM)", RFC 2163, January 1998, https://www.rfc-editor.org/info/rfc2163. PX.
  • [RFC2181] Elz, R. and R. Bush, "Clarifications to the DNS Specification", RFC 2181, July 1997, https://www.rfc-editor.org/info/rfc2181. §5.2, that records in one RRset whose TTLs differ are not thereby different records, which is why Record compares as it does.
  • [RFC2308] Andrews, M., "Negative Caching of DNS Queries (DNS NCACHE)", RFC 2308, March 1998, https://www.rfc-editor.org/info/rfc2308. What the last field of an soa has meant since: how long a denial may be cached, rather than a default TTL.
  • [RFC2535] Eastlake 3rd, D., "Domain Name System Security Extensions", RFC 2535, March 1999, https://www.rfc-editor.org/info/rfc2535. KEY, SIG and NXT — the DNSSEC that came before RFC 4034's, all three of which still decode here.
  • [RFC2782] Gulbrandsen, A., Vixie, P., and L. Esibov, "A DNS RR for specifying the location of services (DNS SRV)", RFC 2782, February 2000, https://www.rfc-editor.org/info/rfc2782. SRV, and its "name compression is not to be used for this field".
  • [RFC2874] Crawford, M. and C. Huitema, "DNS Extensions to Support IPv6 Address Aggregation and Renumbering", RFC 2874, July 2000, https://www.rfc-editor.org/info/rfc2874. A6, an address with its prefix somewhere else.
  • [RFC2930] Eastlake 3rd, D., "Secret Key Establishment for DNS (TKEY RR)", RFC 2930, September 2000, https://www.rfc-editor.org/info/rfc2930. TKEY, the other half of TSIG.
  • [RFC2931] Eastlake 3rd, D., "DNS Request and Transaction Signatures ( SIG(0)s )", RFC 2931, September 2000, https://www.rfc-editor.org/info/rfc2931. sig0: §3's constants — type covered zero, the root as the owner, class any — and §3.1's two-part data, the record's own rdata with the signature omitted rather than zeroed, followed by the message as it stood before the signature was added to it.
  • [RFC3007] Wellington, B., "Secure Domain Name System (DNS) Dynamic Update", RFC 3007, November 2000, https://www.rfc-editor.org/info/rfc3007. What ties update to tsig and sig0: an update is authenticated by a signature on the request rather than by anything inside it, which is why none of the three knows about the other two.
  • [RFC3123] Koch, P., "A DNS RR Type for Lists of Address Prefixes (APL RR)", RFC 3123, June 2001, https://www.rfc-editor.org/info/rfc3123. APL.
  • [RFC3403] Mealling, M., "Dynamic Delegation Discovery System (DDDS) Part Three: The Domain Name System (DNS) Database", RFC 3403, October 2002, https://www.rfc-editor.org/info/rfc3403. NAPTR, whose replacement field §4.1 says is not compressed.
  • [RFC3425] Lawrence, D., "Obsoleting IQUERY", RFC 3425, November 2002, https://www.rfc-editor.org/info/rfc3425. Why opcode 1 is named and has no meaning.
  • [RFC3597] Gustafsson, A., "Handling of Unknown DNS Resource Record (RR) Types", RFC 3597, September 2003, https://www.rfc-editor.org/info/rfc3597. Why unknown is not a failure case: an unrecognized type is carried byte for byte. §4 forbids compressing a name in anything defined after RFC 1035, which is what Builder obeys and what makes a signed record safe to re-encode; §5 is the \# length hex presentation form and the TYPE1234 and CLASS42 spellings.
  • [RFC4025] Richardson, M., "A Method for Storing IPsec Keying Material in DNS", RFC 4025, March 2005, https://www.rfc-editor.org/info/rfc4025. IPSECKEY.
  • [RFC4034] Arends, R., Austein, R., Larson, M., Massey, D., and S. Rose, "Resource Records for the DNS Security Extensions", RFC 4034, March 2005, https://www.rfc-editor.org/info/rfc4034. DS, DNSKEY, RRSIG and NSEC: the key tag arithmetic of Appendix B, the type bitmap of §4.1.2, the serial comparison of an expiry time in §3.1.5, and the canonical name order of §6.1 that Name.order implements.
  • [RFC4343] Eastlake 3rd, D., "Domain Name System (DNS) Case Insensitivity Clarification", RFC 4343, January 2006, https://www.rfc-editor.org/info/rfc4343. Why Name.eql and Name.hash ignore case, and over which bytes.
  • [RFC4398] Josefsson, S., "Storing Certificates in the Domain Name System (DNS)", RFC 4398, March 2006, https://www.rfc-editor.org/info/rfc4398. CERT.
  • [RFC4648] Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, October 2006, https://www.rfc-editor.org/info/rfc4648. The alphabets the presentation forms are written in, §7's extended hex one among them.
  • [RFC5001] Austein, R., "DNS Name Server Identifier (NSID) Option", RFC 5001, August 2007, https://www.rfc-editor.org/info/rfc5001. The nsid option, which says which node answered.
  • [RFC5155] Laurie, B., Sisson, G., Arends, R., and D. Blacka, "DNS Security (DNSSEC) Hashed Authenticated Denial of Existence", RFC 5155, March 2008, https://www.rfc-editor.org/info/rfc5155. NSEC3 and NSEC3PARAM, and the unpadded base32hex their hashes are printed in.
  • [RFC6563] Jiang, S., Conrad, D., and B. Carpenter, "Moving A6 to Historic Status", RFC 6563, March 2012, https://www.rfc-editor.org/info/rfc6563. Why A6 is decoded but nothing should send one.
  • [RFC6672] Rose, S. and W. Wijngaards, "DNAME Redirection in the DNS", RFC 6672, June 2012, https://www.rfc-editor.org/info/rfc6672. DNAME, whose target is not compressed even though it looks like a CNAME's.
  • [RFC6742] Atkinson, RJ, Bhatti, SN, and S. Rose, "DNS Resource Records for the Identifier-Locator Network Protocol (ILNP)", RFC 6742, November 2012, https://www.rfc-editor.org/info/rfc6742. NID, L32, L64 and LP.
  • [RFC6840] Weiler, S., Ed. and D. Blacka, Ed., "Clarifications and Implementation Notes for DNS Security (DNSSEC)", RFC 6840, February 2013, https://www.rfc-editor.org/info/rfc6840. §5.7, what the DO bit means in a query rather than a response.
  • [RFC6891] Damas, J., Graff, M., and P. Vixie, "Extension Mechanisms for DNS (EDNS(0))", STD 75, RFC 6891, April 2013, https://www.rfc-editor.org/info/rfc6891. The opt pseudo-record, which is where a message keeps everything the twelve-byte header had no room for: exactly one of them (§6.1.1), owned by the root (§6.1.2), carrying the payload size, the upper bits of the response code, and the option list.
  • [RFC6975] Crocker, S. and S. Rose, "Signaling Cryptographic Algorithm Understanding in DNS Security Extensions (DNSSEC)", RFC 6975, July 2013, https://www.rfc-editor.org/info/rfc6975. The dau, dhu and n3u options.
  • [RFC7208] Kitterman, S., "Sender Policy Framework (SPF) for Authorizing Use of Domains in Email, Version 1", RFC 7208, April 2014, https://www.rfc-editor.org/info/rfc7208. Why the SPF type is decoded and deprecated: the policy went back to living in a TXT.
  • [RFC7314] Andrews, M., "Extension Mechanisms for DNS (EDNS) EXPIRE Option", RFC 7314, July 2014, https://www.rfc-editor.org/info/rfc7314. The expire option.
  • [RFC7477] Hardaker, W., "Child-to-Parent Synchronization in DNS", RFC 7477, March 2015, https://www.rfc-editor.org/info/rfc7477. CSYNC.
  • [RFC7553] Faltstrom, P. and O. Kolkman, "The Uniform Resource Identifier (URI) DNS Resource Record", RFC 7553, June 2015, https://www.rfc-editor.org/info/rfc7553. URI, whose target §4.5 says must not be empty — one of the length-versus-type checks this library makes and dnspython does not.
  • [RFC7828] Wouters, P., Abley, J., Dickinson, S., and R. Bellis, "The edns-tcp-keepalive EDNS0 Option", RFC 7828, April 2016, https://www.rfc-editor.org/info/rfc7828. The tcp_keepalive option.
  • [RFC7830] Mayrhofer, A., "The EDNS(0) Padding Option", RFC 7830, May 2016, https://www.rfc-editor.org/info/rfc7830. The padding option, which carries nothing and is the point.
  • [RFC7858] Hu, Z., Zhu, L., Heidemann, J., Mankin, A., Wessels, D., and P. Hoffman, "Specification for DNS over Transport Layer Security (TLS)", RFC 7858, May 2016, https://www.rfc-editor.org/info/rfc7858. Port 853, which is the whole of what this library takes from it.
  • [RFC7871] Contavalli, C., van der Gaast, W., Lawrence, D., and W. Kumari, "Client Subnet in DNS Queries", RFC 7871, May 2016, https://www.rfc-editor.org/info/rfc7871. The client_subnet option, and §6's rule that only the significant bits of the address are sent.
  • [RFC7873] Eastlake 3rd, D. and M. Andrews, "Domain Name System (DNS) Cookies", RFC 7873, May 2016, https://www.rfc-editor.org/info/rfc7873. The cookie option and the badcookie response code.
  • [RFC7901] Wouters, P., "CHAIN Query Requests in DNS", RFC 7901, June 2016, https://www.rfc-editor.org/info/rfc7901. The chain option.
  • [RFC8005] Laganier, J., "Host Identity Protocol (HIP) Domain Name System (DNS) Extension", RFC 8005, October 2016, https://www.rfc-editor.org/info/rfc8005. HIP.
  • [RFC8145] Wessels, D., Kumari, W., and P. Hoffman, "Signaling Trust Anchor Knowledge in DNS Security Extensions (DNSSEC)", RFC 8145, April 2017, https://www.rfc-editor.org/info/rfc8145. The key_tag option.
  • [RFC8490] Bellis, R., Cheshire, S., Dickinson, J., Dickinson, S., Lemon, T., and T. Pusateri, "DNS Stateful Operations", RFC 8490, March 2019, https://www.rfc-editor.org/info/rfc8490. The dso opcode and the dsotypeni response code.
  • [RFC8749] Mekking, W. and D. Mahoney, "Moving DNSSEC Lookaside Validation (DLV) to Historic Status", RFC 8749, March 2020, https://www.rfc-editor.org/info/rfc8749. Why DLV is decoded and decommissioned.
  • [RFC8777] Holland, J., "DNS Reverse IP Automatic Multicast Tunneling (AMT) Discovery", RFC 8777, April 2020, https://www.rfc-editor.org/info/rfc8777. AMTRELAY.
  • [RFC8914] Kumari, W., Hunt, E., Arends, R., Hardaker, W., and D. Lawrence, "Extended DNS Errors", RFC 8914, October 2020, https://www.rfc-editor.org/info/rfc8914. The extended_error option: the difference between a server that cannot answer and a server that will not.
  • [RFC8945] Dupont, F., Morris, S., Vixie, P., Eastlake 3rd, D., Gudmundsson, O., and B. Wellington, "Secret Key Transaction Authentication for DNS (TSIG)", STD 93, RFC 8945, November 2020, https://www.rfc-editor.org/info/rfc8945. TSIG, decoded without its MAC ever being checked, and never compressed on the way out so that a verifier sees what was signed. §4.3 is what tsig writes: the request's MAC, the message with its signature removed and its identifier put back, and the variables — or, for the second and later messages of a transfer, §5.3.1's timers alone. §4.2's error field is its own registry, where 16 is badsig and not the badvers the same number means in a header, which is why Tsig.ErrorCode exists.
  • [RFC8976] Wessels, D., Barber, P., Weinberg, M., Kumari, W., and W. Hardaker, "Message Digest for DNS Zones", RFC 8976, February 2021, https://www.rfc-editor.org/info/rfc8976. ZONEMD.
  • [RFC9276] Hardaker, W. and V. Dukhovni, "Guidance for NSEC3 Parameter Settings", BCP 236, RFC 9276, August 2022, https://www.rfc-editor.org/info/rfc9276. Why zero iterations and an empty salt are what an NSEC3PARAM should say.
  • [RFC9460] Schwartz, B., Bishop, M., and E. Nygren, "Service Binding and Parameter Specification via the DNS (SVCB and HTTPS Resource Records)", RFC 9460, November 2023, https://www.rfc-editor.org/info/rfc9460. SVCB and HTTPS: the strictly increasing key order of §2.2, the mandatory check of §8, alias mode and the parameters §2.4.2 says to ignore there, and the keyNNNNN spelling of §2.1.
  • [RFC9461] Schwartz, B., "Service Binding Mapping for DNS Servers", RFC 9461, November 2023, https://www.rfc-editor.org/info/rfc9461. The dohpath service parameter.
  • [RFC9540] Pauly, T. and T. Reddy.K, "Discovery of Oblivious Services via Service Binding Records", RFC 9540, February 2024, https://www.rfc-editor.org/info/rfc9540. The ohttp service parameter.
  • [RFC9567] Arends, R. and M. Larson, "DNS Error Reporting", RFC 9567, April 2024, https://www.rfc-editor.org/info/rfc9567. The report_channel option.
  • [RFC9606] Reddy.K, T. and M. Boucadair, "DNS Resolver Information", RFC 9606, June 2024, https://www.rfc-editor.org/info/rfc9606. RESINFO.
  • [RFC9660] Salgado, H., Vergara, M., and D. Wessels, "The DNS Zone Version (ZONEVERSION) Option", RFC 9660, October 2024, https://www.rfc-editor.org/info/rfc9660. The zoneversion option.
  • [RFC9715] Fujiwara, K. and P. Vixie, "IP Fragmentation Avoidance in DNS over UDP", RFC 9715, January 2025, https://www.rfc-editor.org/info/rfc9715. Why the payload size to advertise is 1232 rather than as much as possible.
  • [RFC9859] Stenstam, J., Thomassen, P., and J. Levine, "Generalized DNS Notifications", RFC 9859, September 2025, https://www.rfc-editor.org/info/rfc9859. DSYNC, and the generalized notify that has a parent re-check a CDS or CSYNC without waiting for a scan.
  • [Z46] Ollie, J., "z46 — IPv4 and IPv6 addresses, networks and endpoints for Zig", 2026, https://git.jcollie.dev/jeff/z46. The only dependency: every address this library hands back — in an a, an aaaa, a wks, an apl, an ipseckey, an SVCB address hint or a client subnet option — is one of its types, parsed and formatted by it.
  • [DNSPYTHON] Halley, B. and the dnspython contributors, "dnspython", version 2.8.0, https://www.dnspython.org/. The second decoder the differential test compares against, message by message, and the source of the stricter of the two compression pointer rules.