systemd-resolved

10. systemd-resolved #

10.1 Managing /etc/resolv.conf content #

The main issue with /etc/resolv.conf is managing it in modern distributions, which can have multiple sources of nameserver and search domain information due to multiple interfaces (both real and virtual, such as VPN tunnels) with concurrent DHCP clients.

The legacy method to handle this complexity was the Resolvconf project. It set up a number of hooks and updated /etc/resolv.conf appropriately by following some defined rules. However, its main issue is its lack of flexibility and inability to manage several nameservers and search domains on a per-link basis, which is sometimes needed in complex setups.

To address this, the emergence of systemd-resolved was intended to resolve (^_^) these issues.

10.2 Integrating systemd-resolved into system #

Slowly but surely, systemd is taking on more features and responsibilities in modern GNU/Linux distributions. One of these responsibilities is serving as a local source of truth for hostname resolution with systemd-resolved.

There are several ways you can start using it system-wide or directly in your application.

The ways systemd-resolved can integrate into the system.
Figure 5. – The ways systemd-resolved can integrate into the system.

For the system wide setup there are currently two ways:

  1. Using nss module resolve (man libnss_resolve.so.2).
  2. Delegate the management of /etc/resolv.conf to systemd-resolved by symlinking /run/systemd/resolve/resolv.conf. The file sets the nameserver to 127.0.0.53 where systemd-resolved is listening and sets search domains.

The second option is preferred because it also addresses issues with the search domain.

But systemd-resolved is not just a management tool for /etc/resolv.conf; it also acts as a local cache. It supports mDNS, /etc/hosts, and other name sources, and can mix them all using predefined and configurable rules, some of which we will discuss below.

By default the systemd uses a link name server if you have only one interface with DHCP:

$ resolvectl
Global
         Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
  resolv.conf mode: stub

Link 2 (eth0)
    Current Scopes: DNS
         Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 192.168.5.3
       DNS Servers: 192.168.5.3
$ resolvectl show-server-state
Server: 192.168.5.3
                              Type: link
                         Interface: eth0
                   Interface Index: 2
            Verified feature level: UDP
            Possible feature level: UDP
                       DNSSEC Mode: no
                  DNSSEC Supported: no
Maximum UDP fragment size received: 512
               Failed UDP attempts: 0
               Failed TCP attempts: 0
             Seen truncated packet: no
          Seen OPT RR getting lost: yes
             Seen RRSIG RR missing: no
               Seen invalid packet: no
            Server dropped DO flag: no

You can change this behavior by setting a DNS recursor in the systemd-resolved config file /etc/systemd/resolved.conf.

# cat /etc/systemd/resolved.conf
[Resolve]
DNS=1.1.1.1
# systemctl restart systemd-resolved
# resolvectl
Global
         Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
  resolv.conf mode: stub
       DNS Servers: 1.1.1.1

Link 2 (eth0)
    Current Scopes: DNS
         Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
       DNS Servers: 192.168.5.3

As you can see now we have a global and a link DNS server.

$ resolvectl show-server-state                                                                                                                                                                                             
Server: 1.1.1.1
                              Type: system
            Verified feature level: UDP+EDNS0
            Possible feature level: UDP+EDNS0
                       DNSSEC Mode: no
                  DNSSEC Supported: no
Maximum UDP fragment size received: 512
               Failed UDP attempts: 0
               Failed TCP attempts: 0
             Seen truncated packet: no
          Seen OPT RR getting lost: no
             Seen RRSIG RR missing: no
               Seen invalid packet: no
            Server dropped DO flag: no

Server: 192.168.5.3
                              Type: link
                         Interface: eth0
                   Interface Index: 2
            Verified feature level: n/a
            Possible feature level: UDP+EDNS0
                       DNSSEC Mode: no
                  DNSSEC Supported: no
Maximum UDP fragment size received: 512
               Failed UDP attempts: 0
               Failed TCP attempts: 0
             Seen truncated packet: no
          Seen OPT RR getting lost: no
             Seen RRSIG RR missing: no
               Seen invalid packet: no
            Server dropped DO flag: no
$ resolvectl dns
Global: 1.1.1.1
Link 2 (eth0): 192.168.5.3

Run a DNS query:

$ resolvectl query facebook.com
facebook.com: 2a03:2880:f158:181:face:b00c:0:25de -- link: eth0
              157.240.214.35                   -- link: eth0

-- Information acquired via protocol DNS in 20.4ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network
$ resolvectl query facebook.com
facebook.com: 157.240.214.35                   -- link: eth0
              2a03:2880:f189:80:face:b00c:0:25de -- link: eth0

-- Information acquired via protocol DNS in 15.2ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: cache network

In order to test a link name resolver we need to install our own DNS server. I suggest using CoreDNS, as it’s simple and powerful for our needs:

$ wget https://github.com/coredns/coredns/releases/download/v1.11.1/coredns_1.11.1_linux_arm64.tgz
$ tar -zxf coredns_1.11.1_linux_arm64.tgz

Configure it with the following config files where we create a zone test.example:

$ cat Corefile
test.example {
        bind 127.0.0.153
        loadbalance round_robin
        file test.example.db
}

And zone file:

$ cat test.example.db
$ORIGIN test.example.
@       3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2017042745 7200 3600 1209600 3600

site    IN      A       127.0.0.1
        IN      A       127.0.0.2
        IN      A       127.0.0.3

Now check the search domains:

$ resolvectl domain
Global:
Link 2 (eth0):

Let’s set it to test.example:

$ resolvectl domain eth0 test.example
# resolvectl domain
Global:
Link 2 (eth0): test.example

The /etc/resolv.conf was updated accordingly:

$ cat /etc/resolv.conf

nameserver 127.0.0.53
options edns0 trust-ad
search test.example  <--------------------------

Setting a name server for eth0:

$ resolvectl dns eth0 127.0.0.153
$ resolvectl
Global
         Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
  resolv.conf mode: stub
Current DNS Server: 1.1.1.1
       DNS Servers: 1.1.1.1 127.0.0.153

Link 2 (eth0)
    Current Scopes: DNS
         Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 127.0.0.153
        DNS Servers: 127.0.0.153
        DNS Domain: test.example

The /etc/resolv.conf still has stub only, as expected:

$ cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
search test.example

Let’s make some queries:

$ resolvectl query site.test.example
site.test.example: 127.0.0.2
                   127.0.0.1
                   127.0.0.3

-- Information acquired via protocol DNS in 3.2ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network
$ resolvectl query facebook.com
facebook.com: 2a03:2880:f158:181:face:b00c:0:25de -- link: eth0
              163.70.147.35                       -- link: eth0

-- Information acquired via protocol DNS in 16.8ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network

Query with search domain for site hostname:

$ resolvectl query site
site: 127.0.0.1
      127.0.0.3
      127.0.0.2
      (site.test.example)

-- Information acquired via protocol DNS in 3.4ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network

For non-existing site2:

$ resolvectl query site2
site3: Name 'site2' not found

In tcpdump we can see that the request was sent with search domain:

# tcpdump -i any -s0 -A -n port 53

17:49:24.467909 lo    In  IP 127.0.0.1.36205 > 127.0.0.153.53: 17745+ [1au] A? site2.test.example. (47)
17:49:24.468073 lo    In  IP 127.0.0.1.46166 > 127.0.0.153.53: 40292+ [1au] AAAA? site2.test.example. (47)

But if we ask for a hostname fb.com, we will see an interesting under the hood:

$ dig +short @127.0.0.53 fb.com
157.240.214.35
$ sudo tcpdump -i any -s0 -A -n port 53
19:08:01.937480 eth0  Out IP 192.168.5.15.51825 > 1.1.1.1.53: 20123+ [1au] A? fb.com. (35)

19:08:01.937501 eth0  Out IP 192.168.5.15.45070 > 1.1.1.1.53: 23151+ [1au] AAAA? fb.com. (35)

19:08:01.937522 lo    In  IP 127.0.0.1.35601 > 127.0.0.153.53: 13671+ [1au] A? fb.com. (35)

19:08:01.937672 lo    In  IP 127.0.0.1.48931 > 127.0.0.153.53: 11867+ [1au] AAAA? fb.com. (35)

19:08:01.937888 lo    In  IP 127.0.0.153.53 > 127.0.0.1.35601: 13671 Refused- 0/0/1 (35)

19:08:01.937909 lo    In  IP 127.0.0.153.53 > 127.0.0.1.48931: 11867 Refused- 0/0/1 (35)

19:08:01.950784 eth0  In  IP 1.1.1.1.53 > 192.168.5.15.51825: 20123 1/0/1 A 157.240.214.35 (51)

19:08:01.952483 eth0  In  IP 1.1.1.1.53 > 192.168.5.15.45070: 23151 1/0/1 AAAA 2a03:2880:f158:181:face:b00c:0:25de (63)

The query returns a correct answer, but in the tcpdump we can see that all name servers were asked to provide responses, including our link nameserve 127.0.0.153. This default behavior could lead to a very nasty DNS leak security flaw usually with VPN software:

A DNS leak is a security flaw that allows DNS requests to be revealed to ISP DNS servers, despite the use of a VPN service to attempt to conceal them.[1] Although primarily of concern to VPN users, it is also possible to prevent it for proxy and direct internet users.

In order to address this issues we should leverage the Domains directive:

[Resolve]
DNS=1.1.1.1
Domains=~.

Domains=

A whitespace-separated list of domains which should be resolved using the DNS servers on this link. Each item in the list should be a domain name, optionally prefixed with a tilde ("~"). The domains with the prefix are called “routing-only domains”. The domains without the prefix are called “search domains” and are first used as search suffixes for extending single-label hostnames (hostnames containing no dots) to become fully qualified domain names (FQDNs). If a single-label hostname is resolved on this interface, each of the specified search domains are appended to it in turn, converting it into a fully qualified domain name, until one of them may be successfully resolved.

Both “search” and “routing-only” domains are used for routing of DNS queries: look-ups for hostnames ending in those domains (hence also single label names, if any “search domains” are listed), are routed to the DNS servers configured for this interface. The domain routing logic is particularly useful on multi-homed hosts with DNS servers serving particular private DNS zones on each interface.

The “routing-only” domain “~.” (the tilde indicating definition of a routing domain, the dot referring to the DNS root domain which is the implied suffix of all valid DNS names) has special effect. It causes all DNS traffic which does not match another configured domain routing entry to be routed to DNS servers specified for this interface. This setting is useful to prefer a certain set of DNS servers if a link on which they are connected is available.

Get the new config:

$ resolvectl
Global
         Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
  resolv.conf mode: stub
       DNS Servers: 1.1.1.1 127.0.0.153
        DNS Domain: ~.

Link 2 (eth0)
    Current Scopes: DNS
         Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
       DNS Servers: 127.0.0.153
       DNS Domain: test.example

And now if we re-run our query:

$ resolvectl query fb.com
fb.com: 2a03:2880:f189:80:face:b00c:0:25de     -- link: eth0
        163.70.147.35                          -- link: eth0

-- Information acquired via protocol DNS in 17.7ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network

And in the tcpdump output we can see only requests to the global resolver:

$ sudo tcpdump -i any -s0 -A -n port 53

19:10:38.312632 eth0  Out IP 192.168.5.15.41775 > 1.1.1.1.53: 37889+ [1au] AAAA? fb.com. (35)

19:10:38.312656 eth0  Out IP 192.168.5.15.50771 > 1.1.1.1.53: 14510+ [1au] A? fb.com. (35)

19:10:38.327208 eth0  In  IP 1.1.1.1.53 > 192.168.5.15.41775: 37889 1/0/1 AAAA 2a03:2880:f189:80:face:b00c:0:25de (63)
E..[

19:10:38.329088 eth0  In  IP 1.1.1.1.53 > 192.168.5.15.50771: 14510 1/0/1 A 163.70.147.35 (51)

And local domains still work:

$ resolvectl query site
site: 127.0.0.2
      127.0.0.1
      127.0.0.3
      (site.test.example)

-- Information acquired via protocol DNS in 1.7ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network

and only ask local name server with the correct search domain:

19:11:51.498355 lo    In  IP 127.0.0.1.43769 > 127.0.0.153.53: 57337+ [1au] AAAA? site.test.example. (46)

19:11:51.498462 lo    In  IP 127.0.0.1.55548 > 127.0.0.153.53: 44925+ [1au] A? site.test.example. (46)

19:11:51.499115 lo    In  IP 127.0.0.153.53 > 127.0.0.1.55548: 44925*- 3/0/1 A 127.0.0.2, A 127.0.0.1, A 127.0.0.3 (145)

19:11:51.499149 lo    In  IP 127.0.0.153.53 > 127.0.0.1.43769: 57337*- 0/1/1 (128)

10.4 Useful commands #

A handy monitor sub command allows us to monitor the ongoing queries:

$ sudo resolvectl monitor
→ Q: cf.com IN AAAA
← S: success
← A: cf.com IN SOA merlin.ns.cloudflare.com dns.cloudflare.com 2343786646 10000 2400 604800 1800

→ Q: google.com IN AAAA
← S: success
← A: google.com IN AAAA 2a00:1450:4009:823::200e

As was mentioned above systemd-resolved provides a local cache:

$ sudo resolvectl show-cache
Scope protocol=dns ifindex=2 ifname=eth0
No entries.

Scope protocol=dns
google.com IN AAAA 2a00:1450:4009:823::200e
google.com IN MX 10 smtp.google.com

10.5 Querying systemd-resolved #

Using standard resolver methods like getaddrinfo() or connecting to /etc/resolv.conf nameservers over the DNS protocol will not provide the full power of per-link resolvers and search domains. Therefore, if your application truly needs this information, you can use two alternative interfaces provided by systemd-resolved:

  1. Varlink
  2. D-Bus

Install dependencies:

$ sudo apt install python3-varlink meson make
$ python3 -m varlink.cli info  unix:/run/systemd/resolve/io.systemd.Resolve
Vendor: The systemd Project
Product: systemd (systemd-resolved)
Version: 255 (255.4-1ubuntu8.1)
URL: https://systemd.io/
Interfaces:
   io.systemd
   io.systemd.Resolve
   org.varlink.service

Build cli tool:

$ git co https://github.com/varlink/libvarlink
$ cd libvarlink
$ make build 

Run:

$ build/tool$ ./varlink help unix:/run/systemd/resolve/io.systemd.Resolve/io.systemd.Resolve

$ ./varlink call unix:/run/systemd/resolve/io.systemd.Resolve/io.systemd.Resolve.ResolveHostname '{ "name": "site"}'
{
  "addresses": [
    {
      "address": [
        127,
        0,
        0,
        1
      ],
      "family": 2
    },
    {
      "address": [
        127,
        0,
        0,
        2
      ],
      "family": 2
    },
    {
      "address": [
        127,
        0,
        0,
        3
      ],
      "family": 2
    }
  ],
  "flags": 8388609,
  "name": "site.test.example"
}

The additional features can be called by specifying the interface by setting ifindex.

10.5.2 D-Bus #

Another way to interact with an extended systemd-resolved interface is via D-Bus.

$ busctl status org.freedesktop.resolve1
PID=3478
PPID=1
TTY=n/a
UID=991
EUID=991
SUID=991
FSUID=991
GID=991
EGID=991
SGID=991
FSGID=991
SupplementaryGIDs=991
Comm=systemd-resolve
CommandLine=/usr/lib/systemd/systemd-resolved
Label=unconfined
CGroup=/system.slice/systemd-resolved.service
Unit=systemd-resolved.service
Slice=system.slice
UserUnit=n/a
UserSlice=n/a
Session=n/a
AuditLoginUID=n/a
AuditSessionID=n/a
UniqueName=:1.42
EffectiveCapabilities=cap_net_raw
PermittedCapabilities=cap_net_raw

Documentation for the API format could be found here: https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.resolve1.html.

Run to resolve site hostname:

$ busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager ResolveHostname isit 0 site 0 0

result:

a(iiay)st 3 0 2 4 127 0 0 3 0 2 4 127 0 0 1 0 2 4 127 0 0 2 "site.test.example" 8388609

Run to resolve fb.com hostname:

$ busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager ResolveHostname isit 0 fb.com 0 0

Result:

a(iiay)st 2 2 2 4 157 240 221 35 2 10 16 42 3 40 128 241 88 0 130 250 206 176 12 0 0 37 222 "fb.com" 8388609
Read next chapter →