DNS for the hopeless
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 (libc
1) 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”.
-
For the purposes of this post, I refer to
libc
as the C POSIX library plus widely agreed-upon extensions (such as BSDlibresolv
) - not the Standard C library. ↩︎ -
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. ↩︎ -
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. ↩︎
-
See 4.3BSD
resolver(3)
andresolver(5)
. ↩︎ -
See
getaddrinfo(3)
,gethostbyname(3)
andhostname(7)
for more information. ↩︎ -
See
nss(5)
andnsswitch.conf(5)
. ↩︎ -
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. Seesrc/network/lookup_name.c
andsrc/network/resolvconf.c
. ↩︎ -
See
resolver(3)
andglibc/resolv/README
forglibc
. ↩︎ -
e.g.
c-ares
and its competitors. ↩︎ -
libcurl
defaults to a threaded resolver backend backed bylibc
. 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. ↩︎ -
See
resolvconf(8)
. There’s also a good overview of its motivations onresolvconf/README.md
. ↩︎ -
See
systemd-resolved.service(8)
,nss-resolve(8)
, andorg.freedesktop.resolve1(5)
. ↩︎