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
.