mDNSResponder is Apple's open source DNS, mDNS, and DNS-SD project which combined with link-local addressing forms their Bonjour zero-configuration networking stack. Apple distributes Bonjour on their own platforms as well as Windows via Bonjour Print Services or as a bundle with iTunes. mDNSResponder also underlies Android's Network Service Discovery API and is packaged on some open-source UNIX operating systems.
In embedded applications mDNSResponder may be integrated as a single monolith but it is more typically deployed as a daemon which applications communicate with via a small client shim. The protocol used between the two sides is documented only in the source code, and this is not an attempt at changing that. Instead it's an introduction to the protocol and relevant source code aimed at someone looking to reimplement the protocol for themselves.
First let's get the code. Apple publish mDNSResponder as a tarball but there's two other distributions worth knowing about. The first is by IETF participants (including Apple engineers) which I'd characterise as a development sneak peek. If you're interested in what's likely to appear in Apple products some time down the line you can find it on GitHub. The second is the Android Open Source Project's fork which has some small changes for the Android platform.
If you're completely new to the project your first stop should be mDNSShared/dns_sd.h
which forms the public API. You'll want to be familiar with DNSServiceRegister
,
DNSServiceBrowse
, DNSServiceResolve
, and DNSServiceGetAddrInfo
which are all self
descriptive as to the operations they start. Once an operation is started
DNSServiceRefSockFD
is used to obtain a file descriptor which will become readable
when a new event is ready. To handle the event call DNSServiceProcessResult
, and when
finished with the operation call DNSServiceRefDeallocate
. For a further introduction
to the public API, see Apple's Introduction to DNS Service Discovery.
Behind these calls a stream is established to the daemon. On Windows it's TCP to
127.0.0.1:5354. Everywhere else it's UDS which by default connects to the
environment variable DNSSD_UDS_PATH
, with a fallback to /var/run/mDNSResponder
. The POSIX Makefile changes the fallback to /var/run/mdnsd
while the Android fork changes it to /dev/socket/mdnsd
.
The stream carries an ad-hoc binary protocol composed of C friendly structures. Integers are in network byte order, strings are printable ASCII terminating with zero, and structs are packed.
All the previously mentioned operations begin with the client sending an ipc_msg_hdr
whose fields are used as follows:
version
- protocol version, currently1
datalen
- length of the op specific data following the headeripc_flags
-1
for requesting no replyop
- identifies the request with a value from therequest_opt_t
enumclient_context
- 8 bytes that are echoed in replies and which is used in the regular API to store a client pointerreg_index
- used to identify records byDNSServiceRegisterRecord
andDNSServiceRemoveRecord
Following the header is operation specific data. You can find the data necessary for an
operation by looking at its implementation in mDNSShared/dnssd_clientstub.c
. For
example the browse op requires:
flags
- a uint32 with a (relevant) set of flags from kDNSServiceFlagsinterfaceIndex
- a uint32 representing a network interface, zero means anyregtype
- a string representing the service typedomain
- a string containing a browse domain name, empty means any
If the request is poorly formed the daemon will close the connection, otherwise it
will respond with an int32 containing a kDNSServiceErr error value. If the value is
kDNSServiceErr_NoError
(zero) the next value off the wire will be a response
CallbackHeader
which consists of an ipc_msg_hdr
- this time
with op
containing a value from the reply_op_t
enum - along with a
uint32 set of flags from kDNSServiceFlags, an uint32 interface index, and an
int32 error from kDNSServiceErr. These last three values all count toward the
ipc_msg_hdr
's datalen
value.
The remaining data will be operation specific. In the case of browse, we can see that in
DNSServiceBrowse
a handler of handle_browse_response
was set, and from
its implementation we can expect a string containing a service name, a string
containing a service type, and a string containing a browse domain. We can determine
whether this service has been found or lost based on whether
kDNSServiceFlagsAdd
is set in the flags recieved in the header, and expect
further sequences like this (without the initial error) as services come and go.
Most calls in the dns_sd.h
API will follow this pattern. There are exceptions like
DNSServiceGetProperty
but you'll discover those as you go source diving to find the
request and response payload parameters.
The last concept worth mentioning is shared connections as created by
DNSServiceCreateConnection
. Shared connections allow multiple operations on a
single stream by moving the initial error response to a seperate stream. On
Windows, this is done by creating a new TCP listening socket and including its port
immediately following the request ipc_msg_hdr
. For UNIX streams, you can
either start a named listener and pass its path as a string following the request header,
or you can create a new socket pair and pass one end to the daemon via SCM_RIGHTS
,
which is indicated by a null byte following the request header.
That's about it for this introduction. From here you'll likely want to spend some more
time leafing through dns_sd.h
, dnssd_clientlib.c
, dnssd_clientstub.c
, dnssd_ipc.h
and dnssd_ipc.c
, all located in mDNSShared
. Let me know how you get on.