DNS 101: the client protocol

A client sends a request (QR query message) to a server and receives a response (QR reply message).

Messages are limited to 512 bytes unless EDNS is supported, servers listen on port 53/udp and reply to the client sending port. DNS-over-TCP/TLS/HTTPS/QUIC are the exception.

The reality

DNS is a central part of the internet, which makes it a delicate beast. The “DNS stack” varies with the system configuration (operating system, programming language runtime).

DNS on C

The standard C library (libc1) provides an API for “protocol-independent nodename and service name translation”. Applications call getaddrinfo() to obtain a list of IPs for a particular host name, and the standard C library takes care of the rest.2 getaddrinfo is part of the C POSIX library, and documented in RFC 3493, section 6.1.

The BIND DNS client library became the de facto standard API for Unix-like operating systems. It defines an API for the stub resolver3 deployment method, along with a configuration format. 4

The GNU C Library (glibc) is the most common libc implementation on Linux. It provides getaddrinfo along with several non-standard (but documented5) extension APIs and, most importantly, is backed by the GNU Name Service Switch (NSS).

The main goal of nss is to provide a configurable and extensible6 facade to internal system databases (e.g. users and groups) whose entries are obtained from multiple sources (e.g. a file, or a networked service) in a given order configured by a policy.

In the case of name resolution, the hosts database is queried. Its (mostly) default policy is to use the file source (/etc/hosts) followed by the dns source (the glibc implementation of the “stub resolver” API).

musl is another popular libc implementation that expectedly differs from glibc, but still preserves relative compatibility by using the same stub resolver configuration format, and mimicking its default policy.7

Finally, the “stub resolver” API is also implemented by both libraries, however there’s no up to date documentation.8

The situation is considerably more complicated if the application uses any non-standard C API for service name translation.9 For example, if you face problems with domain name resolution on cURL you first need to determine whether it’s using c-ares or libc, but switching between them requires re-compiling libcurl.10

In summary: DNS resolution depends entirely on your system’s libc implementation, which will likely use the “stub resolver” deployment method. Its name resolution process might involve different sources and protocols, and will (hopefully) at some point query some DNS recursive resolver. In case of glibc, this process involves the use of the GNU NSS hosts database.

DNS on Linux

In order to actually resolve a name, the “stub resolver” needs to be configured (via resolver(5)) with the network address of the recursive resolver(s) it shall use. However, this configuration model falls short when it comes to dynamic network environments on which multiple programs concurrently and asynchronously supply and use nameserver information.

The Debian’s resolvconf is “a framework for keeping track of the system’s information about currently available nameservers”. By putting itself as “the intermediary between programs that supply nameserver information and applications that use that information”, it takes the role of managing the shared nameserver configuration database.11

The systemd project also (unsurprisingly) provides an alternative to nameserver configuration management, (unsurprisingly) called systemd-resolved, which (unsurprisingly) also does a lot more than just that. It features a “local DNS stub listener”, a NSS plugin intended to replace dns source, and a D-Bus API. 12.

Depending on the system configuration, the standard C library can end up using systemd for name resolution by either using its “local DNS stub listener” as the configured nameserver for its “stub resolver”, or using its NSS plugin to replace the glibc “stub resolver” entirely.

In summary: nameserver configuration can come from multiple sources, and is commonly managed by some implementation of the resolvconf mechanism, but there’s also multiple ways in which this configuration can reach the libc “stub resolver”.

  1. For the purposes of this post, I refer to libc as the C POSIX library plus widely agreed-upon extensions (such as BSD libresolv) - not the Standard C library. ↩︎

  2. Due to being protocol-independent, it’s up to the libc implementation to decide which naming service and protocol to use (and how to implement it), but even when DNS is used as naming service, performing a name translation hardly ever means simply making a DNS query.
    Each implementation has a “resolver algorithm” (RFC 1034, section 5.3.3) which resembles a protocol on its own: read a local database (/etc/hosts), cache positive/negative responses, query multiple nameservers serially/concurrently, etc. ↩︎

  3. The section 5.3.1 of RFC 1034 defines the “stub resolver” as an architectural component that delegates the resolution function to a name server which supports recursive queries. ↩︎

  4. See 4.3BSD resolver(3) and resolver(5)↩︎

  5. See getaddrinfo(3), gethostbyname(3) and hostname(7) for more information. ↩︎

  6. See nss(5) and nsswitch.conf(5)↩︎

  7. i.e. musl also obtains nameserver configuration from /etc/resolv.conf as well as resolving names first from /etc/hosts, followed by an unicast DNS query. See src/network/lookup_name.c and src/network/resolvconf.c↩︎

  8. See resolver(3) and glibc/resolv/README for glibc↩︎

  9. e.g. c-ares and its competitors↩︎

  10. libcurl defaults to a threaded resolver backend backed by libc. For more information, see https://curl.se/docs/faq.html#How_does_libcurl_resolve_host_na and https://everything.curl.dev/libcurl/names#name-resolver-backends↩︎

  11. See resolvconf(8). There’s also a good overview of its motivations on resolvconf/README.md↩︎

  12. See systemd-resolved.service(8), nss-resolve(8), and org.freedesktop.resolve1(5)↩︎