Zeroconf (also previously called Rendezvous) is a protocol for discovering services available on the local network. Software using Zeroconf can ask for all services implementing a particular protocol, and then obtain more detailed information about one particular service instance. Zeroconf is used in Apple's Mac OS X for discovering printers and HTTP servers, and Apple has made their implementation freely available. (See Apple's Zeroconf developer site for more information.)
Zeroconf is small enough to be implementable from scratch; PyZeroconf is a pure Python implementation by Paul Scott Murphy, and is about 1500 lines of code. This article explains the basics of Zeroconf and shows simple examples of PyZeroconf usage.
The Zeroconf Protocol
There are two specifications that define the Zeroconf protocol. First, there's a specification for running the Domain Name Service (DNS) protocol over multicast IP, instead of the more usual unicast IP. A second specification lays out the rules for discovering services through special DNS packets sent over multicast DNS.
Multicast DNS
DNS is one of the core components of the Internet, letting networking machines obtain the IP address corresponding to a hostname. Standard DNS works by sending UDP packets to port 53 on a server; clients send out a query, and get back a set of records with the requested information.
There are several different types of DNS record. Type A records contain the IP address for a hostname (e.g. the host named www.example.org has IP address 192.168.0.1). PTR records are the opposite, listing the hostname corresponding to a given address (e.g. 192.168.0.1 corresponds to www.example.org). SRV records contain information about services available from a domain (e.g. the SSH server for example.org is login.example.org.) TXT records contain arbitrary chunks of text.
(There are more types of DNS records that aren't relevant to Zeroconf and won't be covered here. http://support.algx.net/cst/dns/dns2.html has an informal discussion of the most common record types; RFC 1035 has a complete list of the basic record types. Various RFCs have proposed new types of DNS records; SRV records are defined by RFC 2782.)
The first specification used for Zeroconf is an Internet draft titled draft-cheshire-dnsext-multicastdns.txt.
Multicast DNS uses the same packet structures as regular DNS, but packets are sent to the multicast address 224.0.0.251 and UDP port 5353. The Multicast DNS draft explains how to suppress duplicate answers, cache answers to reduce network traffic, what the TTL on responses should be, and other low-level things that aren't very important for a Zeroconf user (as opposed to an implementor). The draft also defines a new top-level domain for link-local names, .local., and service discovery will use this domain extensively. (The trailing dot on .local. is necessary according to the DNS protocol. Your C library automatically adds it to hostnames for you, but because we'll be working with DNS packets directly, the trailing dot will always be shown in our examples.)
More information on this draft is available from http://www.multicastdns.org. Unfortunately, the IETF standardization status of this protocol is unclear. The draft document expired on August 14th 2004; I don't know if there are plans for an updated draft.
Service Discovery
Before discussing how services are found, we need to explain a few conventions.
Different protocols are identified by strings, such as _http._tcp or _nameserver._udp. The first portion of the name is the name of the protocol; the second portion specifies whether the protocol uses TCP or UDP. Both portions are prefixed with underscores because these protocol strings will be used in DNS queries. Underscores aren't legal in hostnames, so using them in protocol identifiers prevents collisions with actual hosts (say, if you had a server named nameserver.example.org).
For standardized protocols, the name in RFC 1700 should be used; these names are probably identical to the ones in the /etc/services file on your machine. Names for non-standard protocols are listed at http://www.dns-sd.org/ServiceTypes.html. For example, from this list you can learn that Apple's iTunes uses the name _daap._tcp.
An instance of a server on a particular machine is identified by another name, consisting of <Service Identifier>.<Service Name>.<Domain>. Service identifiers are never typed in but will be displayed to the user, so they should be long and descriptive. They can include spaces and punctuation marks. Some example service instance names could be:
Wiki Server._http._tcp.example.org Laser Printer._ipp._tcp.example.org Durus Database (192.168.0.1)._durus._tcp.example.org
Given these naming conventions, we can now look up services by sending out a number of Multicast DNS queries. This process is described in another Internet draft, draft-cheshire-dnsext-dns-sd.txt. This draft also expired on August 14th 2004, but it does describe the protocol as implemented in Apple's code.
As an example, let's examine the queries to look for a particular Durus database.
- Send out a request for PTR records for _durus._tcp.local.. Because of the .local. domain in the request, this query will only go to local servers. Because multicast DNS is used, it will go to all machines on the local network link.
- Get back a set of PTR records that list matching services: "Database 1._durus._tcp.local.", "Database 2._durus._tcp.local.", ...
To get more information about a particular service instance:
- Send out a request for A, SRV, and TXT records for that instance's name.
- Get back a SRV record that includes the address and port of the instance. For example, you'd send a request for Database 1._durus._tcp.local., and get back data.example.org, port 2972.
- A TXT record might also be returned, containing a set of key/value pairs with additional information about the service. For example, a four-colour printer might have pairs such as 'Color':'4', 'Paper-size':'A4', etc.
To make your application announce its presence using Zeroconf, your machine has to respond to DNS queries about the application's services. Someday operating systems other than Mac OS X may include a Zeroconf server; when that day arrives, you would use some OS-specific mechanism to register and unregister your service instances. For now we're stuck running our own miniature DNS server. Luckily, the implementation work has already been done in the PyZeroconf package.
Python Zeroconf Support
PyZeroconf (http://sourceforge.net/projects/pyzeroconf) is a pure Python implementation of Zeroconf. Be sure to get release 0.12 or later; earlier releases have some known bugs.
The primary class is the Zeroconf class, which handles both sending and receiving multicast DNS packets. On creating a Zeroconf instance, a number of threads are spawned to continuously handle received DNS packets and maintain a cache of them. Only one instance needs to be created per process.
Publishing a Service
We'll start by publishing a service because that's very easy. First you create a Zeroconf instance, and then you create a number of ServiceInfo instances and register them with the Zeroconf object:
import Zeroconf
import socket
server = Zeroconf.Zeroconf()
# Get local IP address
local_ip = socket.gethostbyname(socket.gethostname())
local_ip = socket.inet_aton(local_ip)
svc1 = Zeroconf.ServiceInfo('_durus._tcp.local.',
'Database 1._durus._tcp.local.',
address = local_ip,
port = 2972,
weight = 0, priority=0,
properties = {'description':
'Departmental server'}
)
server.registerService(svc1)
That's all it takes to publish a service's existence. It's OK to create and register multiple services with a single Zeroconf object.
Note the conversion required for the address parameter. You must supply a value for address; if you don't, the DNS server will never send out any A records and clients will be unable to determine where the service is running.
The properties parameter is a dictionary with strings as its keys and values. The contents of this dictionary are converted to a textual form described in the dns-sd draft, and made available as a TXT record. Properties can therefore be used to publish additional information about a service; for example, a remote file access protocol might include the path of the exported filesystem, or an FTP server might include the anonymous login to use.
The weight and priority parameters are used by clients to choose between multiple equivalent services. RFC 2782 explains their semantics. In short, clients should always try the service with the smallest priority value; if that service isn't working, the one with the second-smallest priority value should be tried next, and so forth. If there are multiple services with the same priority, a service should be picked at random from all the services of equal priority; larger weight values increase the chance that the service will be chosen. Both values are integers from 0 to 65535.
Discovering Services
To discover what services are available, you use the ServiceBrowser class. ServiceBrowser uses a Zeroconf instance to keep track of services as they come and go, calling addService() and removeService() methods on a listener object.
Here's some example code:
import Zeroconf
class MyListener(object):
def removeService(self, server, type, name):
print "Service", repr(name), "removed"
def addService(self, server, type, name):
print "Service", repr(name), "added"
# Request more information about the service
info = server.getServiceInfo(type, name)
print 'Additional info:', info
if __name__ == '__main__':
server = Zeroconf.Zeroconf()
listener = MyListener()
browser = Zeroconf.ServiceBrowser(server, "_durus._tcp.local.", listener)
The MyListener class could also keep a list of services it's seen, notify other objects when there's a change, or do anything else you like.
When I run the above code after running the publishing example, I get the following output:
Service u'Database 1._durus._tcp.local.' added
Additional info: service[Database 1._durus._tcp.local.,
192.168.0.126:2972,description=Depa...]
DNS records have an expiration time. The default lifetime is an hour, so even if I kill the server, the ServiceBrowser will not notice that the service has gone away until the record has timed out. However, the getServiceInfo() call will fail and return None because nothing will respond to the request for the service's SRV record. This makes it possible to determine if a service is alive or not.
Debugging Tip
Apple's Zeroconf implementation is called mDNSResponder, and is available from http://developer.apple.com/darwin/projects/rendezvous/.
One program in the mDNSPosix directory in the source distribution is called mDNSNetMonitor. mDNSNetMonitor listens to the multicast DNS traffic and prints out all the DNS packets. For example, running mDNSNetMonitor and then running the browser example above results in the following output (my comments added in square brackets):
andrewk@Andrew-iBook2:~/Desktop/mDNSResponder-58.8/mDNSPosix/build$ ./mDNSNetMonitor [[Initial PTR request for protocol]] 10:39:19.047467 192.168.0.126 -Q- Q: 1 Ans: 0 Auth: 0 Add: 0 Size: 35 bytes 192.168.0.126 (QM) PTR _durus._tcp.local. [[Response to PTR request]] 10:39:19.071127 192.168.0.126 -R- Q: 0 Ans: 1 Auth: 0 Add: 0 Size: 71 bytes 192.168.0.126 (AN+) PTR 3600 _durus._tcp.local. -> Database 1._durus._tcp.local. [[From .getServiceInfo(): request for SRV,TXT, and A records]] 10:39:19.296714 192.168.0.126 -Q- Q: 3 Ans: 0 Auth: 0 Add: 0 Size: 58 bytes 192.168.0.126 (QM) SRV Database 1._durus._tcp.local. 192.168.0.126 (QM) TXT Database 1._durus._tcp.local. 192.168.0.126 (QM) Addr Database 1._durus._tcp.local. [[SRV, TXT, A responses]] 10:39:19.327597 192.168.0.126 -R- Q: 0 Ans: 2 Auth: 0 Add: 1 Size: 120 bytes 192.168.0.126 (AN) SRV 3600 Database 1._durus._tcp.local. -> Database 1._durus._tcp.local.:2972 192.168.0.126 (AN) TXT 3600 Database 1._durus._tcp.local. -> description=Departmental server 192.168.0.126 (AD) Addr 3600 Database 1._durus._tcp.local. -> 192.168.0.126
References
http://developer.apple.com/macosx/rendezvous Apple's developer site for Zeroconf/Rendezvous.
http://developer.apple.com/darwin/projects/rendezvous/ Download site for Apple's Zeroconf implementation.
http://www.multicastdns.org Site for multicast DNS specification and related materials.
http://www.dns-sd.org Site for DNS-based service discovery.
http://sourceforge.net/projects/pyzeroconf Zeroconf implementation in Python, written by Paul Scott Murphy.
Acknowledgements
Andrew Dalke noted that v0.12 of PyZeroconf switched from using the name Rendezvous to Zeroconf.