DEBOPS.RESOLVED(5)

NAME
debops.resolved - Manage Network Name Resolver using Ansible

SYNOPSIS

debops run service/resolved [--limit group,host,...] [--diff] [--check] [--tags tag1,tag2,...] [--skip-tags tag1,tag2,...] [<ansible-playbook options>] ...

DESCRIPTION

The systemd-resolved(8) <https://manpages.debian.org/systemd-resolved(8)> service is a part of the systemd <https://www.freedesktop.org/wiki/Software/systemd/> project. The service is a local DNS resolver that can manage the /etc/resolv.conf configuration file and provide name resolution, Multicast DNS and LLDP capabilities, among other things.

The debops.resolved Ansible role manages the systemd-resolved configuration. Other parts of the systemd ecosystem are managed by separate Ansible roles.

GETTING STARTED

  Support for other DNS resolvers

The role checks if one of the APT packages listed in the resolved__skip_packages variable is already installed on the host before doing any modification on the system. If such package is detected, the role will skip further tasks to avoid messing up existing configuration.

  Fallback DNS configuration

Since Debian 12 (Bookworm), the systemd-resolved service is provided via a separate package, which on installation automatically replaces the /etc/resolv.conf file with a symlink. The default Debian installation still uses the ifupdown package to configure networking, which results in broken DNS resolution on systemd-resolved installation because the service does not get the relevant nameserver information from ifupdown scripts.

To mitigate this, the debops.resolved role will get the current DNS configuration from Ansible facts and add it in the /etc/systemd/resolved.conf.d/00fallback-dns.conf file as "Fallback DNS configuration" before installing the service itself. This should avoid issues with DNS before the actual configuration is defined. The file can be safely removed later, or its configuration will be overridden if specified in the subsequent configuration files.

  Management of the /etc/resolv.conf config file

The /etc/resolv.conf configuration file can be overwritten by the dhclient command, used by the ifupdown scripts which are installed by default when Debian Installer is used to deploy a host. To ensure that network configuration and name resolution are not impacted, the debops.resolved role will symlink its own generated resolv.conf file only when the systemd-networkd service is detected via the Ansible local fact managed by the debops.networkd role.

Refer to the /ETC/RESOLF.CONF section of the systemd-resolved.service(8) <https://manpages.debian.org/systemd-resolved.service(8)> manual page for more information on how the service interacts with the config file.

  Example inventory

The debops.resolved role is included by default in the common.yml DebOps playbook; you don't need to add hosts to any Ansible groups to enable it.

  Example playbook

If you are using this role without DebOps, here's an example Ansible playbook that uses the debops.resolved role:

---
- name: Manage systemd-resolved service
  collections: [ 'debops.debops', 'debops.roles01',
                 'debops.roles02', 'debops.roles03' ]
  hosts: [ 'debops_all_hosts', 'debops_service_resolved' ]
  become: True
  environment: '{{ inventory__environment | d({})
                   | combine(inventory__group_environment | d({}))
                   | combine(inventory__host_environment  | d({})) }}'
  roles:
    - role: etc_services
      tags: [ 'role::etc_services', 'skip::etc_services' ]
      etc_services__dependent_list:
        - '{{ resolved__etc_services__dependent_list }}'
    - role: resolved
      tags: [ 'role::resolved', 'skip::resolved' ]

  Ansible tags

You can use Ansible --tags or --skip-tags parameters to limit what tasks are performed during Ansible run. This can be used after a host was first configured to speed up playbook execution, when you are sure that most of the configuration is already in the desired state.

Available role tags:

role::resolved
    Main role tag, should be used in the playbook to execute all of the role tasks as well as role dependencies.

DNS-SD SUPPORT IN SYSTEMD-RESOLVED

  Overview

The systemd-resolved service supports DNS Service Discovery <https://en.wikipedia.org/wiki/Zero-configuration_networking#DNS-SD> using Multicast DNS <https://en.wikipedia.org/wiki/Multicast_DNS> protocol, similar to the one used by Avahi <https://en.wikipedia.org/wiki/Avahi_(software)> on Linux and Bonjour <https://en.wikipedia.org/wiki/Bonjour_(software)> on Apple macOS environments. Services can be advertised on a .local <https://en.wikipedia.org/wiki/.local> DNS domain and discovered by compatible clients. The resolvectl query and resolvectl service commands can be used to find local services advertised by DNS-SD.

  Network configuration for Multicast DNS

For the mDNS support to work, it needs to be enabled globally in systemd-resolved (it is enabled by default). Then, support needs to be enabled on specific network interfaces using systemd-networkd to tell systemd-resolved on which interfaces it should advertise and listen for DNS-SD traffic.

Here's an example network configuration for a host with Ethernet interface which makes sure that Multicast DNS support is enabled. Configuration is applied on the host using the debops.networkd Ansible role:

---
# File: ansible/inventory/host_vars/laptop/networkd.yml
networkd__configuration
  - name: 'eth0.network'
    raw: |
      [Match]
      Name=eth0
      [Network]
      DHCP=yes
      MulticastDNS=yes
      [DHCPv4]
      UseDomains=yes
    state: 'present'

After configuring the network interface(s), users can check the state of Multicast DNS using the resolvectl command. An example output:

user@laptop:~$ resolvectl
Global
       Protocols: +LLMNR +mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Link 2 (eth0)
Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6 mDNS/IPv4 mDNS/IPv6
     Protocols: +DefaultRoute +LLMNR +mDNS -DNSOverTLS DNSSEC=no/unsupported
   DNS Servers: 192.0.2.1
    DNS Domain: example.org

The +mDNS flag in the "Global" section indicates that Multicast DNS is enabled in systemd-resolved service. The same flag in the "Link" section indicates that Multicast DNS traffic is accepted on a particular link.

Users also need to make sure that the mDNS multicast UDP traffic is accepted by the firewall. The port to open is 5353/udp (defined as mdns in /etc/services database) and the destination addresses are 224.0.0.251 for IPv4 network and ff02::fb for IPv6 network. This configuration should be automatically enabled by the debops.ferm role included in DebOps.

  Exploring the .local network

When Multicast DNS support is enabled, it should be possible to ping other hosts on the .local domain:

user@laptop:~$ ping -c 1 server.local
PING server.local (192.0.2.12) 56(84) bytes of data.
64 bytes from server.example.org (192.0.2.12): icmp_seq=1 ttl=64 time=0.841 ms
--- server.local ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.841/0.841/0.841/0.000 ms

The resolvectl query command can be used to find out what services are advertised on the local network. Currently they will only show services advertised on the same host the command is executed on:

user@laptop:~$ resolvectl query -p mdns --type=PTR _services._dns-sd._udp.local
_services._dns-sd._udp.local IN PTR _workstation._tcp.local -- link: eth0
_services._dns-sd._udp.local IN PTR _ssh._tcp.local         -- link: eth0
_services._dns-sd._udp.local IN PTR _sftp-ssh._tcp.local    -- link: eth0
-- Information acquired via protocol mDNS/IPv6 in 2.9ms.
-- Data is authenticated: yes

A specific service type can be queried as well:

user@laptop:~$ resolvectl query -p mdns --type=PTR _ssh._tcp.local
_ssh._tcp.local IN PTR laptop._ssh._tcp.local               -- link: eth0
-- Information acquired via protocol mDNS/IPv6 in 457us.
-- Data is authenticated: yes

Unfortunately, current UI for service discovery in systemd-resolved is limited, there's no user-facing way to list discovered services <https://github.com/systemd/systemd/issues/14796> known to the resolver. Users can debug this currently using journald logs. In one terminal, start viewing the logs of the systemd-resolved service:

user@laptop:~$ sudo journalctl -f -u systemd-resolved.service

In another terminal, send the USR1 signal to the service to dump its cache information in the logs:

user@laptop:~$ sudo killall -USR1 systemd-resolved

This should display information about other hosts seen in the .local network. The systemd project developers are working on an user interface <https://github.com/systemd/systemd/pull/18355> for this functionality, it might be available in the future.

If the hostname of a given service is known, the resolvectl service command can be used to find out its SRV resource records published in DNS:

user@laptop:~$ resolvectl service server._ssh._tcp.local
server._ssh._tcp.local: server.local:22 [priority=0, weight=0]
                        192.0.2.12                        -- link: eth0
                        (server/_ssh._tcp/local)
-- Information acquired via protocol mDNS/IPv4 in 238.6ms.
-- Data is authenticated: no

Users should be able to use the services as normal:

user@laptop:~$ ssh server.local
The authenticity of host 'server.local (192.0.2.12)' can't be established.
ECDSA key fingerprint is SHA256:fy8ZGpDIc2a4Zmd2eIcbGDyJttN4eY0pRMZeUl1S7No.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'server.local,192.0.2.12' (ECDSA) to the list of known hosts.
You have no mail.
Last login: Fri Mar  3 12:24:57 2023 from laptop.example.org
user@server:~$

  Publishing services using DNS-SD

To publish a service using DNS Service Discovery protocol, users can put configuration files in the /etc/systemd/dnssd/ directory. The format of the configuration files is described in the systemd.dnssd(5) <https://manpages.debian.org/systemd.dnssd(5)> manual page. The services will be published after the systemd-resolved service is restarted (there's no support for reloading).

An example configuration file which publishes the SSH service on port 22/tcp:

# File: /etc/systemd/dnssd/ssh.dnssd
[Service]
Name=%H
Type=_ssh._tcp
Port=22

The "Name=" parameter will be used as the DNS Resource Record, this is not a descriptive name. The %H variable will be expanded as the hostname.

The debops.resolved role can be used to generate and publish these files, see the resolved__units documentation for more details. The role publishes a few services by default, you can find their configuration in the resolved__default_units variable.

Alternatively, Ansible roles can handle the files themselves; just ensure that the /etc/systemd/dnssd/ directory is present on the host and after the file is created, restart the systemd-resolved service. DebOps provides a convenient handler for this in the debops.global_handlers role.

  Compatibility with Avahi

The avahi-daemon service and the DNS-SD publisher functionality of the systemd-resolved service are mutually exclusive and cannot work reliably at the same time. To fix this issue, the debops.avahi role configures the systemd-resolved service to turn off mDNS support via systemd unit file override. This unfortunately breaks the resolvectl query and resolvectl service support for the .local domain. Hostname resolution should still work via Avahi, and local services can be published the usual way - refer to the debops.avahi role documentation for details.

DEFAULT VARIABLE DETAILS

Some of the debops.resolved default variables have more extensive configuration than simple strings or lists, here you can find documentation and examples for them.

  resolved__configuration

The resolved__*_configuration default variables define the configuration of the systemd-resolved service itself. You can find more details about systemd-resolved configuration in the resolved.conf(5) <https://manpages.debian.org/resolved.conf(5)> manual page.

The generated configuration will be located in the /etc/systemd/resolved.conf.d/ansible.conf config file. This file is not generated by default to fall back on the default configuration provided in the OS packages; to generate it the resolved__deploy_state variable needs to be set to present.

  Examples

Define a set of global DNS servers and search domains for a given host or cluster (consider defining this in systemd-networkd service for per-link DNS servers, or via DHCP):

---
# File: ansible/inventory/group_vars/all/resolved.yml
resolved__deploy_state: 'present'
resolved__configuration:
  - DNS: [ '8.8.8.8', '8.8.4.4' ]
  - Domains: [ 'example.org', 'example.com' ]

Include additional DNS servers on a specific host:

---
# File: ansible/inventory/host_vars/<hostname>/resolved.yml
resolved__host_configuration:
  - DNS: [ '1.1.1.1', '1.0.0.1' ]

Do the same as above, but don't include the default list of DNS servers by resetting the configuration entry:

---
# File: ansible/inventory/host_vars/<hostname>/resolved.yml
resolved__host_configuration:
  - DNS: ''
  - DNS: [ '1.1.1.1', '1.0.0.1' ]

Define a list of additional DNS stub listener addresses on which systemd-resolved service should be available. This is defined by specifying the DNSStubListenerExtra option multiple times, therefore the raw parameter needs to be used for this to work. The name parameter is ignored in this case:

resolved__deploy_state: 'present'
resolved__configuration:
  - name: 'stub_listeners'
    raw: |
      DNSStubListenerExtra=192.0.2.10
      DNSStubListenerExtra=2001:db8:0:f102::10
    state: 'present'

You can check the resolved__default_configuration variable for the default contents of the configuration file.

  Syntax

The role uses the Universal Configuration system to configure systemd-resolved daemon. Each configuration entry in the list is a YAML dictionary. The simple form of the configuration uses the dictionary keys as the parameter names, and dictionary values as the parameter values. Remember that the parameter names need to be specified in the exact case they are used in the documentation (e.g. SpeedMeterInterval, DUIDType), otherwise they will be duplicated in the generated configuration file. It's best to use a single YAML dictionary per configuration option.

If the YAML dictionary contains the name key, the configuration switches to the complex definition mode, with configuration options defined by specific parameters:

name
    Required. Specify the name of the systemd-networkd configuration file parameter. The case is important and should be the same as specified in the configuration file or the networkd.conf(5) <https://manpages.debian.org/networkd.conf(5)> manual page, otherwise the configuration entries will be duplicated.

    Multiple configuration entries with the same name parameter are merged together in order of appearance. This can be used to modify parameters conditionally.

value
    Optional. The value of a given configuration option. It can be a string, number, True/False boolean, a YAML list of strings or an empty string. Values from multiple name configuration entries are override each other except for YAML lists, in which case they are additive by extending the existing list in order of appearance. To rest a YAML list value, an empty string can be specified as a value of the entry.

raw Optional. String or YAML text block with resolved.conf(5) <https://manpages.debian.org/resolved.conf(5)> configuration which will be included in the generated configuration file as-is. If specified, the name and value of a given entry are not used in the generated configuration.

state
    Optional. If not specified or present, a given configuration parameter will be present in the generated configuration file. If absent, a given parameter will be removed from the configuration file. If comment, the parameter will be present but commented out.

    If the state is init, the parameter will be "primed" in the configuration pipeline, but it will be commented out in the generated configuration file. Any subsequent configuration entry with the same name will switch the state to present - this is used to define the default parameters in the role which can be changed via the Ansible inventory.

    If the state is ignore, a given configuration entry will not be evaluated during role execution. This can be used to activate configuration entries conditionally.

  resolved__units

The resolved__*_units default variables can be used to manage the DNS Service Discovery configuration files used by systemd-resolved service, located in the /etc/systemd/dnssd/ directory. You can find more information about the files themselves in the systemd.dnssd(5) <https://manpages.debian.org/systemd.dnssd(5)> manual page.

  Examples

You can check the resolved__default_units variable for the default set of configuration files maintained by the role.

  Syntax

The role uses the Universal Configuration system to manage systemd-resolved unit files. Each configuration entry in the list is a YAML dictionary, with configuration options defined by specific parameters:

name
    Required. Name of the systemd-resolved unit file to manage. The name can be in the form <unit.type> to denote a single systemd.unit(5) <https://manpages.debian.org/systemd.unit(5)>, as well as <unit.type>.d/<override>.conf to denote a single "override" configuration file that changes the configuration of a specific unit.

    Unit files are stored under the /etc/systemd/dnssd/ subdirectory, automatically created if necessary. The role will create the <unit.type>.d/ subdirectories as needed.

    The name parameter needs to be unique. Multiple configuration entries with the same name parameter are merged together and override each other.

raw Optional. YAML text block in the INI format, with the systemd-resolved configuration options which will be included in the generated configuration file as-is. The text block can contain Jinja statements to generate parts of the configuration dynamically.

    See the systemd.dnssd(5) <https://manpages.debian.org/systemd.dnssd(5)> manual page for details about the unit configuration syntax and available options. The generated files are not validated by Ansible before being applied in the systemd-resolved configuration at this time.

state
    Optional. If not specified or present (default), a given unit configuration file will be generated by Ansible and placed in the systemd-resolved configuration directory. The service will be automatically restarted on changes.

    If absent, the role will remove the specified unit configuration file as well as the override directory if it is present (all override files will be removed, even ones not managed by Ansible). Specific override files can be removed as well if they are defined directly in the name parameter.

    If init, the configuration entry will be prepared, but no changes will be done on the host itself. This can be done to prepare a unit configuration and activate it conditionally later in the universal configuration pipeline.

    If ignore, a given configuration entry will not be evaluated during role execution.

comment
    Optional. String or YAML text block with comments included in the generated configuration file.

SEE ALSO

•   Manual pages: systemd-resolved.service(8) <https://manpages.debian.org/systemd-resolved.service(8)>, resolvectl(1) <https://manpages.debian.org/resolvectl(1)>, nss-resolve(8) <https://manpages.debian.org/nss-resolve(8)>

•   Arch Linux Wiki: systemd-resolved <https://wiki.archlinux.org/title/systemd-resolved>

•   The Sisyphean Task Of DNS Client Config on Linux <https://tailscale.com/blog/sisyphean-dns-client-linux/>

•   Anatomy of a Linux DNS Lookup <https://zwischenzugs.com/2018/06/08/anatomy-of-a-linux-dns-lookup-part-i/>

•   Switching to systemd-resolved for mDNS <https://brokkr.net/2022/07/03/switching-to-systemd-resolved-for-mdns/>

AUTHOR
Maciej Delmanowski

COPYRIGHT
2014-2024, Maciej Delmanowski, Nick Janetakis, Robin Schneider and others