Andrew Tunnell-Jones / Log
2019-11-05

mDNSResponder IPC Protocol

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:

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:

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.