Name resolution, DNS, and systemd-resolved

Follow application name resolution through resolver configuration and caching.

15 min readChapter 116 of 168Intermediate → advanced
In this chapter

Before you start#

Review NSS lookup, D-Bus, network configuration, and UDP and IP. The source examples use systemd v256 and glibc 2.40 as separately pinned historical baselines. The model does not inspect host resolver settings or send DNS queries.

Learning goals#

Trace name resolution across application, NSS and resolver boundaries; explain split-DNS scope selection and transaction lifetime; and distinguish authenticated DNS data from encrypted transport. Analyze a synthetic per-link routing case without assuming every failed upstream causes fallback to every other link.

Source walkthrough#

A name through lookup and scope selection
A name through lookup and scope selectionCorporate name: Matches the example route-only suffix. → VPN scope: Selected in the stated synthetic case. (specific suffix). Public name: Does not match the example corporate suffix. → Default scope: Eligible for the unrelated example name. (default route)specific suffixdefault routeCorporate nameMatches the exampleroute-only suffix.Public nameDoes not match the examplecorporate suffix.VPN scopeSelected in the statedsynthetic case.Default scopeEligible for the unrelatedexample name.
Corporate nameMatches the example route-only suffix.
VPN scopeSelected in the stated synthetic case.
Public nameDoes not match the example corporate suffix.
Default scopeEligible for the unrelated example name.
Read the text version
  • Corporate name: Matches the example route-only suffix. → VPN scope: Selected in the stated synthetic case. (specific suffix)
  • Public name: Does not match the example corporate suffix. → Default scope: Eligible for the unrelated example name. (default route)
Conceptual model. A split-DNS teaching model, not an observed query; real NSS and scope selection depend on configuration.

An application can call the libc getaddrinfo interface to translate a name and service into candidate socket addresses. That interface is not itself a guarantee that a DNS packet was sent. The configured NSS hosts sources can consult local files, a DNS backend, a resolved integration or other modules in configured order. A literal address, a hosts-file match, a cache result, a backend error or a network DNS exchange can produce different paths. The application must handle multiple addresses and failures and release returned results with the corresponding API.

The following exact excerpt is from systemd v256 src/resolve/resolved-dns-query.c, dns_query_go; the file carries an LGPL-2.1-or-later SPDX notice. The preceding state check and following scope traversal are omitted:

c
        r = dns_query_try_etc_hosts(q);
        if (r < 0)
                return r;
        if (r > 0) {
                dns_query_complete(q, DNS_TRANSACTION_SUCCESS);
                return 1;
        }

The query object q first attempts the applicable local hosts-file path. A negative result propagates an error. A positive result completes the query successfully and returns; the subsequent scope-selection loop is not reached on that branch. A zero result allows processing to continue. This is a conditional path in the selected source, not a claim that every application lookup uses resolved or that every hosts entry is accepted in every configuration.

The next exact excerpt is from v256 src/resolve/resolved-dns-transaction.c, dns_transaction_go, also LGPL-2.1-or-later. The surrounding timestamp setup and subsequent transmission are omitted:

c
        r = dns_transaction_prepare(t, ts);
        if (r <= 0)
                return r;

t is a resolver transaction and ts is the time obtained in the surrounding function. A nonpositive preparation result returns immediately. The source comment immediately above states that a zero result can mean the transaction finished immediately and that the transaction and query candidate must not then be accessed. A caller that assumes a returned pointer remains live after synchronous completion risks a lifetime error. A positive pending path has different callback and cleanup obligations. This is source evidence, not a captured DNS request.

Resolver files and service boundaries#

/etc/resolv.conf is a conventional resolver configuration path. On different systems it can be a regular file or a symlink to a generated file; its contents and owner depend on distribution and manager choices. A resolvconf-style update framework is not synonymous with the systemd-resolved service. A NetworkManager integration, another DHCP client, a VPN client or an administrator can supply resolver information under a particular system's configuration. Reading one pathname does not prove who generated it or that all applications use it.

When configured, systemd-resolved can expose a local DNS stub at 127.0.0.53, while other arrangements can provide upstream addresses directly or use NSS/IPC integration. A loopback address belongs to a network namespace: a container's 127.0.0.53 is its own loopback, not automatically the host service. A copied resolver file pointing at a host-only stub can therefore fail in a container unless the corresponding service and routing are available there. Do not infer the resolver path from the existence of a systemd package or assume all applications use libc; a program may use its own resolver.

Loopback addresses depend on the namespace
Host namespace
A resolved stub on 127.0.0.53 answers this network namespace when that service is configured.
Container namespace
The same address is that namespace loopback, so a copied resolver file can fail there.
Own resolver
A program with its own resolver can skip libc, NSS and resolv.conf entirely.
Conceptual model. Conceptual comparison from the chapter prose on systemd v256 resolver boundaries; 127.0.0.53 is the common stub address, and this is not host state.

A DnsQuery represents a higher-level request, potentially with candidates and scopes. A DnsTransaction represents work for a particular question and scope under the implementation's rules. A manager owns collections of scopes and cache state; links and their DNS settings can change while an asynchronous query is pending. Callbacks and references must be coordinated when a link disappears, a request is canceled, a transaction finishes synchronously or a reply arrives late. An object reference can keep memory allocated without guaranteeing that the link or upstream server is still usable.

Split DNS and a concrete model#

A split-DNS configuration can associate domains and DNS servers with different links. Search domains can supply suffixes for applicable single-label names; route-only domains guide routing without supplying search suffixes. For unicast DNS, systemd-resolved chooses the matching routing domain with the most labels. It sends a query to DNS servers associated with that best match; if multiple links have the same best match, their servers are queried in parallel. If no routing domain matches, servers on links marked DefaultRoute=yes and global DNS servers are candidates. These are documented routing rules, not a guarantee that every upstream failure retries every other scope; protocol, query flags, configuration and version still matter. systemd-resolved(8), “Protocols and Routing”

In src/resolve/resolved-dns-scope.c, systemd v256, LGPL-2.1-or-later (the function body is omitted; the comment is copied verbatim):

c
        /* This returns the following return values:
         *
         *    DNS_SCOPE_NO         → This scope is not suitable for lookups of this domain, at all
         *    DNS_SCOPE_LAST_RESORT→ This scope is not suitable, unless we have no alternative
         *    DNS_SCOPE_MAYBE      → This scope is suitable, but only if nothing else wants it
         *    DNS_SCOPE_YES_BASE+n → This scope is suitable, and 'n' suffix labels match
         *
         *  (The idea is that the caller will only use the scopes with the longest 'n' returned. If no scopes return
         *  DNS_SCOPE_YES_BASE+n, then it should use those which returned DNS_SCOPE_MAYBE. It should never use those
         *  which returned DNS_SCOPE_NO.)
         */

The comment defines the match levels that dns_scope_good_domain returns. The caller uses the longest suffix match and uses DNS_SCOPE_MAYBE scopes only when no suffix match exists. The comment does not show how the label count is computed, so it does not by itself confirm the man page's wording for every configuration.

Use a synthetic setup: VPN link V has route-only domain ~corp.example and DNS server 192.0.2.53; ordinary link W has a default-route DNS scope with server 198.51.100.53. For the fully qualified query build.corp.example, assume the route-only suffix is the best eligible match and V is available. The model selects V's scope, subject to actual implementation conditions. A query for www.example.net does not match the corporate suffix and can use an eligible default-route scope under the stated configuration. The addresses and domains are invented; no host configuration was consulted.

In src/resolve/resolved-dns-query.c, dns_query_go, LGPL-2.1-or-later (the hosts-file branch and the code after this loop are omitted):

c
        LIST_FOREACH(scopes, s, q->manager->dns_scopes) {
                DnsScopeMatch match;

                match = dns_scope_good_domain(s, q, q->flags);
                assert(match >= 0);
                if (match > found) { /* Does this match better? If so, remember how well it matched, and the first one
                                      * that matches this well */
                        found = match;
                        first = s;
                }
        }

This loop keeps the first scope that has the highest match value. The code after the loop, which is not shown, handles the no-match case and adds candidates at the best level. The excerpt does not show how a particular domain is scored.

Scope choice in dns_query_go
  1. Score scopes

    dns_scope_good_domain returns a match level for each scope and question.

  2. Best level

    The loop keeps the first scope with the highest level; the longest suffix match ranks highest.

  3. Equal level

    Other scopes at the best level are added as candidates for the same query.

  4. No match

    Without a matching scope the query completes through a synthesized-reply path, not transactions.

Source map. Simplified source map of systemd v256 resolved-dns-query.c and resolved-dns-scope.c; scoring details are omitted and no query was sent.

Now suppose V disappears after the query is created. At T0 the application issues the request; at T1 resolved selects an eligible scope and prepares a transaction; at T2 the link goes away before a reply; at T3 the transaction is canceled or fails according to the actual state machine and available alternatives. A late reply or callback must not revive a freed transaction or silently be attributed to a new link with a reused identity. The specific result and retry choices require the exact source path and configuration. This timeline illustrates an ownership question, not a claim that the v256 implementation always returns one particular error.

A privacy counterexample occurs if a private corporate name is sent to an unintended public resolver due to incorrect routing configuration or an application using a different resolver path. Do not claim that every timeout automatically leaks to every server: the actual scope and fallback rules must be examined. A suffix match can be more specific than a default route, and several equally suitable scopes or protocol choices can exist under particular configurations. The v256 resolved-dns-scope.c contains scope-matching logic; a two-link sketch is not a complete reproduction of it.

Transactions, caching and trust#

A DNS transaction carries a question, selected scope, state, timing and protocol-specific information. Replies must be associated with the correct outstanding request and interpreted under server, transport and validation rules; a transaction ID by itself is not a global authentication token. A resolver can retry or time out and may use TCP in a truncation-related path, subject to implementation and protocol. An invalid, late or mismatched response is not interchangeable with a valid answer. Multiple clients can depend on related resolver work, so canceling one request does not automatically justify freeing state still referenced by another.

In the inspected v256 transaction code, one UDP receive path validates the packet as an appropriate reply and ignores it when its ID differs from the transaction ID before calling reply processing. The DNS branch notes that its connected UDP socket supplies address and port filtering through the kernel; the mDNS and LLMNR branches separately check link interface and address family. A truncated DNS reply can trigger a TCP retry in the applicable path, while a truncated TCP reply is treated as invalid. These are conditional implementation details, not a claim that an ID alone authenticates a DNS answer or that every protocol and transport follows one identical validation sequence. Question matching, server selection and validation depend on additional code and protocol state.

In src/resolve/resolved-dns-transaction.c, on_dns_packet, LGPL-2.1-or-later (the receive call and the reply validation before this check are omitted):

c
        if (DNS_PACKET_ID(p) != t->id) {
                log_debug("Received packet with incorrect transaction ID, ignoring.");
                return 0;
        }

        dns_transaction_process_reply(t, p, false);

A UDP reply whose ID differs from t->id is ignored before dns_transaction_process_reply runs. The check follows dns_packet_validate_reply, but the excerpt does not show question matching or server identity, so an ID match alone should not be read as authentication.

A negative answer can be cached under the applicable DNS and resolver rules with a bounded lifetime. Negative caching reduces repeated traffic but can make a recently added record appear absent until the relevant cached result expires or changes. A timeout, server failure and authoritative negative answer are not the same result and need not have the same caching behavior. A positive cached answer can also become stale relative to an upstream change. The query's time, name, type, scope and cache policy are part of the evidence.

DNSSEC validation can authenticate DNS data under its chain-of-trust and configuration assumptions. Transport encryption, such as DNS-over-TLS when configured, protects a different boundary between client and server. An encrypted channel to an untrusted server does not automatically authenticate the truth of all returned data, and an authenticated DNS answer does not by itself encrypt the query from observers. Local IPC permissions and trust in the local resolver remain separate questions. Multicast DNS and LLMNR can expose names on a local network and are conditional on configuration and link behavior; this chapter does not emit either kind of traffic.

In glibc, getaddrinfo and NSS dispatch involve separately versioned userspace code, including the pinned nss/getXXbyYY_r.c template and resolver-related source. The exact hosts backend and getaddrinfo route should be verified for the selected build rather than inferred from the passwd lookup template. Applications can bypass libc's configured path entirely. A correct observation records the actual application, library, namespace, resolver and source versions, not just the name of a distribution.

Following a libc result without assuming a packet#

In pinned glibc 2.40 nss/getaddrinfo.c, the address-resolution implementation handles NSS status values including success, not-found, unavailable and retry conditions in its lookup machinery. This source is distinct from the passwd-oriented template cited above. A successful lookup can be assembled from an NSS backend's result, and an error or retry depends on that backend and configuration; seeing getaddrinfo in application source does not establish which backend ran. The application's returned list contains addresses with a lifetime managed through the documented allocation and free interface. A pointer into that list must not be used after the list is freed.

A libc lookup with a local short-circuit
A libc lookup with a local short-circuit1. Application to libc NSS: getaddrinfo(name). 2. libc NSS to libc NSS: hosts entry answers locally. 3. libc NSS to resolved: query when there is no local answer. 4. resolved to Upstream DNS: transaction to the selected server. 5. Upstream DNS to resolved (return): reply checked by ID and validation rules. 6. resolved to libc NSS (return): answer or error. 7. libc NSS to Application (return): address list or error.Applicationlibc NSSresolvedUpstream DNSgetaddrinfo(name)hosts entryanswers locallyquery when there isno local answertransaction to theselected serverreply checked by IDand validation rulesanswer or erroraddress list orerror
  1. Applicationlibc NSS

    getaddrinfo(name)

  2. libc NSSlibc NSS

    hosts entry answers locally

  3. libc NSSresolved

    query when there is no local answer

  4. resolvedUpstream DNS

    transaction to the selected server

  5. Upstream DNSresolved

    Return. reply checked by ID and validation rules

  6. resolvedlibc NSS

    Return. answer or error

  7. libc NSSApplication

    Return. address list or error

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Application → libc NSS: getaddrinfo(name)
  2. libc NSS → libc NSS: hosts entry answers locally
  3. libc NSS → resolved: query when there is no local answer
  4. resolved → Upstream DNS: transaction to the selected server
  5. Upstream DNS → resolved (return): reply checked by ID and validation rules
  6. resolved → libc NSS (return): answer or error
  7. libc NSS → Application (return): address list or error
Conceptual model. Synthetic arrangement for illustration; the real hosts order, NSS modules and resolved integration depend on configuration. Not a captured trace.

The selected resolved transaction source contains reply validation and completion paths. A malformed response can lead to an invalid-reply result; DNSSEC validation can produce another failure, while a valid negative response is still semantically different from a timeout. The transaction owns or references packet, server and event-loop state under its implementation's rules. When a pending request is canceled, a timer, socket event or callback may already be queued. The completion path must coordinate those references so that a callback cannot continue using an object whose lifetime has ended. The cited source's explicit immediate-completion warning is a concrete example of that ownership concern; it does not establish the exact cleanup sequence for every transport.

For a second synthetic state table, assume a positive corporate answer with a 60-second TTL enters the selected scope's cache at time 100. Under this simplified model it is eligible until time 160, subject to cache policy and validation. A separate negative answer with its own applicable negative TTL must not inherit that positive answer's lifetime merely because the names look related. If the VPN link disappears at time 120, a cached answer does not prove that packets to the returned address are still routable. If an application has already copied an address into its own state, clearing a resolver cache does not automatically revoke that copy. These examples use invented values and omit many cache and configuration details.

Split routing also interacts with search expansion. Suppose an application asks for unqualified build and a configured search domain is corp.example. A resolver may form a qualified candidate under its documented rules, but whether that candidate is generated, which scope is selected and what happens next depend on the actual NSS backend, query flags and resolver configuration. A route-only domain can guide routing without automatically supplying the same search behavior. The fully qualified and unqualified cases therefore need separate evidence. This distinction matters for privacy: an unintended candidate can reveal an internal label to a server that the operator did not expect.

A private DNS wire parser, if one is later written, must check message lengths, label boundaries and compression-pointer targets, including loops and pointers outside the message, before following them. A few bytes that appear to encode a name are not enough to trust a packet. The exercise below intentionally avoids packet parsing and network access, so it cannot establish that any parser correctly handles malformed compression or adversarial responses.

Practice#

This pure Python model selects between two invented suffix routes. It does not parse untrusted DNS packets, perform name resolution, contact a server or inspect the host's resolver configuration:

sh
python3 - <<'PY'
def scope(name, vpn_up):
    name = name.lower().rstrip('.')
    if name == 'corp.example' or name.endswith('.corp.example'):
        return 'vpn' if vpn_up else 'no modeled scope'
    return 'default'
print('synthetic corporate:', scope('build.corp.example', True))
print('synthetic public:', scope('www.example.net', True))
print('synthetic VPN unavailable:', scope('build.corp.example', False))
PY

Expected output is vpn, default and no modeled scope. The unavailable case deliberately does not invent an automatic fallback. This model omits actual resolved matching, DNS packets, multiple equal scopes, search expansion and caching. If Python is unavailable, evaluate the same three names using the supplied table. No network or host settings are touched.

Source-only alternative: draw application, NSS, resolved query, selected scope, transaction and upstream as distinct objects. Trace the corporate name through T0–T3, mark the local-hosts success branch and immediate-completion lifetime warning, and explain the privacy counterexample. Label both servers and all outcomes synthetic. Deliver the diagram and a table separating negative answer, timeout, cancellation and link removal.

Knowledge check#

  1. Why does getaddrinfo not prove that a DNS packet was sent?
  2. What happens when dns_query_try_etc_hosts returns a positive value in the quoted code?
  3. Why must a caller heed the source warning about a transaction that finishes immediately?
  4. Under the stated model, which scope handles the corporate name and why is the VPN-down result not a universal fallback claim?
  5. How do DNSSEC validation and encrypted DNS transport protect different things?

Answers#

  1. NSS can use local files, a cache or another backend, and an application can use a different resolver implementation. The interface reports address results, not an obligatory network trace.
  2. The query is completed successfully and the function returns on that branch before the subsequent scope selection loop.
  3. The source says the transaction and query candidate must not be accessed after the indicated synchronous completion. Their lifetime can end before a caller expecting only asynchronous completion resumes.
  4. The model chooses VPN V for the matching route-only corporate suffix when it is available. Its unavailable outcome is an explicit simplification; actual fallback depends on version, configuration and eligible scopes.
  5. DNSSEC can authenticate DNS data under its validation assumptions, while transport encryption protects a communication channel. One does not automatically provide the other's property or establish trust in every local component.

Checkpoint#

Submit the split-DNS scope table, T0–T3 lifetime timeline and either the private model output or the source-only explanation. Ready means distinguishing NSS from a network query, explaining both source branches, and identifying the privacy and cancellation uncertainty. Retry with one name and one scope before adding VPN loss and caching.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.