DNS-SD support in systemd-resolved

Overview

The systemd-resolved service supports DNS Service Discovery using Multicast DNS protocol, similar to the one used by Avahi on Linux and Bonjour on Apple macOS environments. Services can be advertised on a .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 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 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) 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.