I’m renaming the series to “Replacing Avahi” because after a bit of reflection “getting rid of” sounds a lot harsher than I ever intended.

In part 1 we took a quick look at what DNS-SD is and why we use Avahi for it on Linux. We then came up with a plan on how to replace it by re-implementing its D-Bus API ourselves by in turn leveraging systemd-resolved’s D-Bus API.

Before we jump into implementing things it’s helpful to familiarise ourselves with how DNS-SD works in general and explore how to do DNS-SD service discovery using both DNS queries and the D-Bus API.

DNS-SD

DNS-SD is essentially a convention of using DNS PTR, SRV and TXT records to enable us to discover services and devices that provide those services.

Each service type you might be interested in has a well-known name made up from its protocl, for example _ipp for the Internet Printing Protocol. Fairly often these can also be vendor names, like _sonos. Append the transport: one of _udp or _tcp and tack on a .local for multicast DNS and we’re in business.

This means that if you want to find devices in your network that are IPP compatible, you start by sending out a request for PTR records with the following query using resolvectl:

$ resolvectl query -p mdns --type=PTR _ipp._tcp.local
_ipp._tcp.local IN PTR SamsungC480W._ipp._tcp.local

Alternatively, using dig:

$ dig +short -p 5353 @224.0.0.251 -t PTR _ipp._tcp.local
SamsungC480W._ipp._tcp.local.

Multicast DNS happens over port 5353 on multicast address 224.0.0.251. We need to supply those to dig, but resolvectl knows what to do based on -p mdns.

Once you have the device name, you can use an SRV query to find out on which port it provides the service, and a TXT query to get all kinds of additional data about the device that can be useful to present to an end user:

$ resolvectl query -p mdns --type=SRV SamsungC480W._ipp._tcp.local
SamsungC480W._ipp._tcp.local IN SRV 0 0 631 SamsungC480W.local
$ resolvectl query -p mdns --type=TXT SamsungC480W._ipp._tcp.local
SamsungC480W._ipp._tcp.local IN TXT "txtvers=1" "note=" "ty=Samsung C48x Series" "rp=ipp/print" "qtotal=1" <...>

The question then becomes, how do I find out the service types? As usual, IANA maintains a registry for this. You can reconstruct any PTR query by following the standard of _ + service name + ._ + transport protocol + .local. If the transport protocol is missing it’s usually tcp in practice, but you can try both and find out.

However, trying out every single one of them and seeing if they exist in your network is rather tedious. Instead, we can use a query to a special service as specified in RFC 6763 Section 9. Knowing that, we can discover services in our network this way:

$ resolvectl query --cache=no -p mdns --type=PTR _services._dns-sd._udp.local
_services._dns-sd._udp.local IN PTR _http._tcp.local
_services._dns-sd._udp.local IN PTR _apt_proxy._tcp.local
_services._dns-sd._udp.local IN PTR _ssh._tcp.local
_services._dns-sd._udp.local IN PTR _smb._tcp.local
_services._dns-sd._udp.local IN PTR _nfs._tcp.local
_services._dns-sd._udp.local IN PTR _nut._tcp.local
_services._dns-sd._udp.local IN PTR _homekit._tcp.local
_services._dns-sd._udp.local IN PTR _sonos._tcp.local
_services._dns-sd._udp.local IN PTR _spotify-connect._tcp.local
_services._dns-sd._udp.local IN PTR _raop._tcp.local
_services._dns-sd._udp.local IN PTR _airplay._tcp.local
_services._dns-sd._udp.local IN PTR _hap._tcp.local

This list is usually not complete, because different devices respond at different speeds. You want to rerun this query multiple times and the finally run the query one more time but with --cache=no omitted. That should give you a reasonably complete list.

D-Bus

Lets try and do what we just did with DNS queries and the resolvectl CLI, but this time through the D-Bus API of systemd-resolved!

The ResolveRecord method lets us resolve any DNS record. The method signature is int32:ifindex, string:name, uint16:class, uint16:type and uint64:flags.

The ifindex is set to 0 meaning any suitable interface, the name should be service name, 1 for the IN(ternet) class and 12 for a PTR record in type. We don’t need to pass any flags to change resolver behaviour, so 0 will do.

$ dbus-send --system --print-reply --dest=org.freedesktop.resolve1 \
   /org/freedesktop/resolve1 \
   org.freedesktop.resolve1.Manager.ResolveRecord \
   int32:0 string:"_ipp._tcp.local" uint16:1 uint16:12 \
   uint64:0

method return time=<...> sender=:1.6 -> destination=:1.391 serial=225 reply_serial=2
   array [
      struct {
         int32 2
         uint16 1
         uint16 12
         array of bytes [
            04 5f 69 70 70 04 5f 74 63 70 05 6c 6f 63 61 6c 00 00 0c 00 01 00
            00 0e 20 00 1e 0c 53 61 6d 73 75 6e 67 43 34 38 30 57 04 5f 69 70
            70 04 5f 74 63 70 05 6c 6f 63 61 6c 00
         ]
      }
   ]
   uint64 1048584

If you decode the array of bytes you’ll find SamsungC480W._ipp._tcp.local inside of it.

In the response the 2 is the ifindex the query happened over, 1 is still the class and 12 is still the type. The 1048584 are flags returning information about the resolver operation.

Now that we’ve found our printer again, we can resolve the service. Though it’s possible for us to resolve the SRV, TXT and A/AAAA using individual calls to ResolveRecord, there’s a more conventient method called ResolveService that will do all of this for us.

Here we need to pass in ifindex:int32, name:string, type:string, domain:string followed by int32:family and finally uint64:flags. The family we’ll use is 2, aka AF_INET (AF_INET6 would be 10).

It should be possible to use AF_UNSPEC (a value of 0) to do lookups for both IPv4 and IPv6, but this always results in an error.

$ dbus-send --system --print-reply --dest=org.freedesktop.resolve1 \
   /org/freedesktop/resolve1 \
   org.freedesktop.resolve1.Manager.ResolveService \
   int32:0 string:"SamsungC480W" string:"_ipp._tcp" string:"local" \
   int32:2 \
   uint64:0

method return time=<...> sender=:1.6 -> destination=:1.417 serial=258 reply_serial=2
   array [
      struct {
         uint16 0
         uint16 0
         uint16 631
         string "SamsungC480W.local"
         array [
            struct {
               int32 2
               int32 2
               array of bytes [
                  <...>
               ]
            }
         ]
         string "SamsungC480W.local"
      }
   ]
   array [
      array of bytes "txtvers=1"
      array of bytes "note="
      array of bytes "ty=Samsung C48x Series"
      array of bytes "rp=ipp/print"
      array of bytes "qtotal=1"
      <...>
   ]
   string "SamsungC480W"
   string "_ipp._tcp"
   string "local"
   uint64 8388616

The first array is an array of SRV records representing priority, weight and port, followed by the hostname to contact and then an array of IP records with the ifindex, the family and the address as a byte array. Then follows the canonicalised hostname.

After that comes an array of TXT records for every key/value pair, followed by the service name, type and domain. The last parameter are flags that return information about the resolver operation.

Recap

Now you know the basics of DNS-SD, how to do lookups yourself and how to use the D-Bus API systemd-resolved exposes. In the next post we’ll take a look at implementing the ServiceBrowserNew, ServiceResolverNew, RecordBrowserNew and ResolveService APIs from org.freedesktop.Avahi.Server.