We had an application that required parsing received DNS response packets for queries made by other applications. DNS packet structure is a 12-byte header, followed by question entries in the question format, followed by entries in answer, authoritative, and additional sections in the resource record (RR) format. The header specifies the number of entries in each of the four sections. Encoding and Decoding are made a bit more elaborate by introduction of pointers: to save space in packets, domain names compressed: encoded such that a domain name can reference as its suffix, some suffix of a prior domain name.
The project required a parser that was permissively licensed, high-performance, mature, easy-to-use, and secure. This sounds like a tall order, but there are some strong candidates! Wikipedia has a page the compares DNS server implementations, complemented by a Google search for DNS client libraries in C. We then filtered by permissive license.
Below is an overview of a survey of the parsing implementations in different projects.
djbdns. The dns library interface parses IPv4, reverse IPv4, MX and TXT records, and the dns_packet library decodes compressed domain names. Both interfaces seem to rely heavily on dynamically allocated memory.
dbndns. It was hard to find the code for dbndns; Ubuntu looked to contain the original plus patches.
MaraDNS. Has two server implementations: MaraDNS an authoritative, non-recursive daemon, whose goal is to serve DNS records to recursive resolvers, and Deadwood, a non-authoritative, recursive DNS daemon to help clients resolve domain names. MaraDNS developement started in 2001, Deadwood in 2007.The two components share no code, and the README claims the Deadwood code is cleaner. Deadwood looks like solid code with just a handful of sources. dwx_dissect_packet() in DwRecurse.c holds logic for handling incoming packets, although the code malloc()s memory for strings and structs.
NSD. Packet parsing code packet_read_rr() in packet.c seems to allocate memory using a custom memory allocator (“region_alloc()”). It also interact with a domain_table_type object (defined in namedb.h), i.e., code does not seem to cleanly separate parsing from processing. dns.h has macros to help parse packets.
Unbound. The client library, libunbound, implements an asynchronous reolver, and process_answer() in libunbound.c handles an incoming packet when it arrives. Handling is performed by process_answer() -> process_answer_detail() -> libworker.c: libworker_enter_result() -> parse_reply() -> util/data/msgparse.c: parse_packet(), which parses the header and calls parse_section() on sections, so parse_packet() seems to be the most relevant entry point to considering Unbound. At the end process_answer() calls the user-supplied callback function. The code uses its own buffer implementation, sldns_buffer.
YADIFA. Relatively recent implementation (started 2012) by the .eu TLD aiming to be “clean, small, light and RFC compliant”. YAFIDA seems to be developed internally by the TLD: the download page offers a tar.gz and de-emphasizes the github repo link, and the repo contains just a handful of commits, one for each release. YADIFA has four libraries: dnscore, dnsdb,dnszone, and dnslg. dnscore seems to include packet parsing infrastructure in message.h; the MESSAGE_ macros access aspects of the header, and message_process() unpacks into a struct message_data, however message_process() appears to mix packet access using the macros with server-specific logic. Specifically, it drops all answer packets (for the parsing use-case we would want to parse these too).
c-ares. Client library used by libcurl, wireshark, node.js and more. Developed on github. Macros for parsing headers, questions and RRs are in ares_dns.h. ares_expand_name() in ares.h and ares_expand_name.c decodes compressed DNS names into a buffer it allocates. ares_parse_a_reply() shows very nicely how to process A replies, with support for following CNAME records. It populates a list of all IP addresses supplied and the resolved hostname and its aliases via the CNAME chain. This seems easy to use; the only potential problem is multiple memory allocations.
dns.c. This is a full recursive, asynchronous DNS client in one c file, supposedly making it easy to embed. Has a github mirror. The h file seems to have everything needed to decode packets, but the code carries no documentation. It is admirably clear in naming and style, and low level functions stand out: dns_rr_parse(), dns_a_parse(), etc., but the sparse comments make it hard to find the right high-level functionality to parse packets. It might be that the resolver interface contains much of the incoming DNS packet processing logic (e.g., what to do when a packet contains multiple RRs).
udns. Has quite detailed documentation. Packet parsing is separate from the communication logic: the user supplies dns_parse_fn(), a callback to parse DNS packets; this callback is called when the reply arrives, after qualifying its DN, query class and qurry type as matching the original request. The library contains parsers for responses of standard query types, including A, AAAA, MX, and more. For A queries see dns_*_a4(), specifically dns_parse_a4. The “low-level interface” has parsing functions, including a domain decoder that doesn’t allocate memory, dns_getdn(). The low-level parsing code has everything required to parse a DNS packet quickly: after dns_initparse(), the user calls dns_nextrr() repeatedly to get the content of the RRs; if given the desired domain name (i.e., the DN in the query), the library performs automatic CNAME expansion.
s6-dns. Initial concern is of the library’s maturity. It came up on a Google search like SPCDNS and was not cross-mentioned in other libraries’ documentation (unlike c-ares, dns.c, udns). The documentation looks good, with separate libraries and s6dns_message_parse_answer_a() in the s6dns_message component, however the library has a dependency on a set of infrastructure libraries, skalibs, and uses skalibs’ dynamically allocated buffer stralloc.
SPCDNS. The project page is dead, there are few recent commits as of writing, and there are only two contributors, but the package focuses on parsing rather than performing the entire query, and does not allocate memory. The library’s dns.h has an impressive collection of structs for RR types. However a comment asking the user to choose which size to use when parsing packets, and to choose it large enough to avoid segfaults is worrying, and together with the fact that the sizes to be allocated are computed as multiple of a uintptr_t (i.e., 1 and 2) and the library wants a uintptr_t * as input..?