mdns User Guide
Introduction
mdns adds multicast DNS service discovery, also known as zeroconf or bonjour to node.js. It provides an object based interface to announce and browse services on the local network.
Internally, it uses the dns_sd API which is available on all major platforms. However, that does not mean it is equally well supported on all platforms. See Compatibility Notes for more information.
The API is documented in the reference section below.
Tutorial
Before we begin go to the internet and get you a bonjour browser so that you can ALL the service discovery.
Multicast DNS service discovery is a solution to announce and discover services on the local network. Here is how to announce a HTTP server running on port 4321:
var mdns = require('mdns')
, ad = mdns.createAdvertisement(mdns.tcp('http'), 4321)
;
ad.start();
A good place to do this is the 'listening'
event handler of your HTTP server. Here is how to browse all HTTP servers on the local network:
var browser = mdns.createBrowser(mdns.tcp('http'));
browser.on('serviceUp', function(service) { console.log("service up: ", service); }); browser.on('serviceDown', function(service) { console.log("service down: ", service); });
browser.start();
As you can see the browser object is an EventEmitter
. For each HTTP server a 'serviceUp'
event is emitted. Likewise, if a server disappears 'serviceDown'
is sent. The service
object of a 'serviceUp'
event might look like this:
{ interfaceIndex: 4
, name: 'somehost'
, networkInterface: 'en0'
, type: {name: 'http', protocol: 'tcp', subtypes: []}
, replyDomain: 'local.'
, fullname: 'somehost._http._tcp.local.'
, host: 'somehost.local.'
, port: 4321
, addresses: [ '10.1.1.50', 'fe80::21f:5bff:fecd:ce64' ]
}
In fact you might receive more than one event per service instance. That is because dns_sd reports each available network path to a service. Also, note that you might get more (or less) addresses. This depends on the network topology. While testing you will often run both peers on the same machine. Now, if you have both a wired and a wireless connection to the same network you will see both addresses on both interfaces (not including IPv6 addresses). The number of IP addresses also depends on the platform and the resolver being used. More on this later.
The name
property is not necessarily the host name. It is a user defined string specifically meant to be displayed in the user interface. It only defaults to the host name.
Note that the examples above intentionally omit error handling. See Error Handling below on how to deal with synchronous and asynchronous errors.
On service types
Service type identifiers are strings used to match service instances to service queries. A service type always contains the service name and the protocol. Additionally it may contain one or more subtype identifiers. Here are some examples:
_http._tcp
_osc._udp
_osc._udp,_api-v1,_api-v2
That’s an awful lot of underscores and punctuation. To make things easier mdns has a helper class, called ServiceType
and some utility functions like mdns.tcp(...)
in the example above. Here are some ways to create a ServiceType
object:
var r0 = mdns.tcp('http') // string form: _http._tcp
, r1 = mdns.udp('osc', 'api-v1') // string form: _osc._udp,_api-v1
, r2 = new mdns.ServiceType('http', 'tcp') // string form: _http._tcp
, r3 = mdns.makeServiceType('https', 'tcp') // string form: _https._tcp
;
Wherever mdns calls for a serviceType
argument you can pass a ServiceType
object or any of the following representations:
var r0 = '_http._tcp,_api-v1' // string form
, r1 = ['http', 'tcp', 'api-v1'] // array form
, r2 = {name: 'http', protocol: 'tcp', subtypes: ['api-v1']} // object form
;
In fact all of these are legal constructor arguments for ServiceType
. JSON (de-)serialization works too. And finally there is makeServiceType(...)
which turns any representation into a ServiceType
object unless it already is one.
Note: mdns liberally makes up service types for testing purposes and it is probably OK if you do the same for your media art project or something. But if you ship a product you should register your service type with the IANA.
Subtypes
TBD.
TXT Records
Each service has an associated DNS TXT record. The application can use it to publish a small amount of metadata. The record contains key-value pairs. The keys must be all printable ascii characters excluding ‘=’. The value may contain any data.
The TXT record is passed to the Advertisement
as an object
:
var txt_record = {
name: 'bacon'
, chunky: true
, strips: 5
};
var ad = mdns.createAdvertisement(mdns.tcp('http'), 4321, {txtRecord: txt_record});
Non-string values are automatically converted. Buffer
objects as values work too.
The size of the TXT record is very limited. That is because everything has to fit into a single DNS message (512 bytes)1. The documentation mentions a “typical size” of 100-200 bytes, whatever that means. There also is a hard limit of 255 bytes for each key-value pair. That’s why they also recommend short keys (9 bytes max). The bottom line is: Keep it brief.
DNS distinguishes between keys with no value and keys with an empty value:
var record = {
empty: ''
, just_a_flag: null // or undefined
};
When browsing for services, the incoming TXT record is automatically decoded and attached to the txtRecord
property of the service
object.
Now, what to put into a TXT record? Let’s start with what not to put in there. You should not put anything in the TXT record that is required to successfully establish a connection to your service. Things like the protocol version should be negotiated in-band whenever possible. Multicast DNS is pretty much a local thing. If your application relies to much on mDNS it will not work in a wide area network. So, just think twice before depending on the TXT record. That said, the TXT record may be used to help with legacy or proprietary protocols. Another application is to convey additional information to the user. Think about a printer dialog. It is very helpful to display the printers location, information about color support &c. before the user makes a choice.
1 This is not entirely accurate. It is possible to use larger TXT records. But you should read the relevant sections of the internet draft before doing so.
The Resolver Sequence
The Browser
object uses a resolver sequence to collect address and port information. A resolver sequence is basically just an array of functions. The functions are called in order and receive two arguments: a service
object to decorate and a next()
function. Each function gathers information on the service, often by invoking asynchronous operations. When done the data is stored on the service
object and the next function is invoked by calling next()
. This is kind of like web server middleware as it happens between service discovery and emitting the events. On the other hand it is just another async function chain thing.
Resolver sequence tasks (RSTs) are created by calling factory functions:
var sequence = [
mdns.rst.DNSServiceResolve()
, mdns.rst.DNSServiceGetAddrInfo({families: [4] })
];
A browser with a custom sequence is created like this:
var browser = mdns.createBrowser(mdns.tcp('http'), {resolverSequence: sequence});
And of course you can write your own tasks:
function MCHammer(options) {
options = options || {};
return function MCHammer(service, next) {
console.log('STOP!');
setTimeout(function() {
console.log('hammertime...');
service.hammerTime = new Date();
next();
}, options.delay || 1000);
}
}
Although it seems a bit complicated this design solves a number of problems:
- The default behavior to resolve all services down to the IP is very convenient. But it is very expensive and very time consuming too. Many applications just don’t need every port number and IP address for every service instance. They just need one. Now it is up to the user to plug together whatever the application requires.
- Portability was another issue. Not all platforms support all functions required by the (old) default behavior. The resolver sequence provides the necessary abstraction to handle this cleanly.
- Something with separation of concerns.
Network Interfaces
Sometimes it is necessary to restrict a browser or advertisement to a certain network interface. Sometimes the service is only available on one interface like on a router. Or maybe you want to run your mdns stuff on the loopback interface to keep it from interfering with a production system. Restricting operation to the loopback interface is also very handy in unit tests.
Browser
and Advertisment
both support a networkInterface
option. It may be set to network interface name, an IP address or an interface index. All three variants have different properties:
Network Interface Names
These are the same names as returned by os.networkInterfaces()
. They are persistent accross reboots, as far as I know even if hot pluggable network adapters (think USB to ethernet) are involved. They are human readable. However, please note that they are not portable across platforms. On Linux ethernet interfaces have an eth
prefix while on darwin (and probably other BSDs) en
is used. Also note that on windows the interface names are localized and, to make things worse, user configurable.
When browsing the service
object passed to the event listeners has a networkInterface
property. It contains the human readable name of the network interface the service was discovered on. See the service object example above.
On windows interface names are only available on vista and better.
IP Addresses
IP addresses are portable across platforms. In an environment that uses dynamic addresses (DHCP, mdns, zeroconf or MS autoconf) they are not necessarily persistent across reboots or disconnects. Also, note that currently simple string comparison is used to find the address in the result returned by os.networkInterfaces()
. This works great for IPv4 addresses. However, IPv6 addresses have multiple equivalent string representations. That means at present you have to use the exact same string as found in the result of os.networkInterfaces()
. Otherwise mdns will fail to find the corresponding interface. This makes IPv6 addresses less portable than IPv4 ones.
On windows IP addresses are only available on vista and better.
Interface Indices
The underlying library dns_sd uses interface indices to identify network interfaces. It is a one-based index as returned by the if_nametoindex(...)
family of calls. It is not a valid index into the list returned by os.networkInterfaces()
because node intentionally skips interfaces that have no address assigned or are down. Passing zero as networkInterface
means “do the right thing” or, to put it simple “listen on all interfaces”. Refer to the dns_sd API documentation under Further Reading for details. This is the default behavior.
Special Case: The Loopback Interface
Newer versions of dns_sd do not use the interface index of the loopback interface to specify local operation. They use the constant kDNSServiceInterfaceIndexLocalOnly
. To write cross platform code that works on most versions of dns_sd you should use the function mdns.loopbackInterface()
like so:
var browser = mdns.createBrowser( mdns.tcp('http')
, { networkInterface: mdns.loopbackInterface()});
On current versions of Mac OS X a Browser
listening on the loopback interface will still discover all services running on the local host. To discover only services that are announced on the loopback interface you’ll have to do some filtering in your serviceUp
and serviceDown
listeners. Just ignore any event where service.interfaceIndex
does not equal mdns.loopbackInterface()
. This is the most portable approach.
As far as I can tell avahi’s dns_sd compatibility library does not support operation on the loopback interface. Neither using the appropriate interface index nor passing the constant seems to work. If you happen to know how to do it please get in touch.
Please note that setting networkInterface
to the loopback name or passing 127.0.0.1 results in undefined behavior. It may work on some platforms and/or versions and fail on others. Even worse, it may just do nothing useful without reporting an error.
Error Handling
In production code error handling is probably a good idea. Synchronous errors are reported by throwing exceptions. EventEmitters report asynchronous errors by emitting an error
event. Asynchronous functions report errors by invoking their callback with an error object as first argument.
Here is an example of an advertisement that is automatically restarted when an unknown error occurs. This happens for example when the systems mdns daemon is currently down. All other errors, like bad parameters, &c. are treated as fatal.
var ad;
function createAdvertisement() { try { ad = mdns.createAdvertisement(mdns.tcp('http'), 1337); ad.on('error', handleError); ad.start(); } catch (ex) { handleError(ex); } }
function handleError(error) { switch (error.errorCode) { case mdns.kDNSServiceErr_Unknown: console.warn(error); setTimeout(createAdvertisement, 5000); break; default: throw error; } }
All errors generated by the underlying dns_sd library have an errorCode
property. Feel free to extend the code above to treat other errors as non-fatal. See the API documentation under Further Reading for a list of error codes. Errors generated by mdns itself do not have error codes. Adding a maximum retry count is left as an exercise for the reader.
Reference
Many arguments and options in mdns are directly passed to the dns_sd API. This document only covers the more important features. For in depth information on the API and how zeroconf service discovery works refer to Further Reading.
mdns.Advertisement
An Advertisement
publishes information about a service on the local network.
The hack0r takes a good look at the local network, someones local network and sprinkles it with fairydust. He watches the particles being swirled up into vortices originating in the passing network traffic. Datadevils on a parking lot next to the information freeway. Visible entropy. The vortices are illuminated by open ports and the pale neon light of multicast DNS service advertisements. The hack0r smiles.
new mdns.Advertisement(serviceType, port, [options], [callback])
Create a new service advertisement with the given serviceType
and port
. The callback
has the arguments (error, service)
and it is run after successful registration and if an error occurs. If the advertisement is used without a callback an handler should be attached to the 'error'
event. The options
object contains additional arguments to DNSServiceRegister(...)
:
- name
- up to 63 bytes of Unicode to be used as the instance name. Think iTunes shared library names. If not given the host name is used instead.
- interfaceIndex
- one-based index of the network interface the service should be announced on. Deprecated: Use networkInterface instead.
- networkInterface
- the network interface to use. See Network Interfaces for details.
- txtRecord
- an object to be published as a TXT record. Refer to the TXT record section for details.
- host
- see documentation of
DNSServiceRegister(...)
- domain
- see documentation of
DNSServiceRegister(...)
- flags
- see documentation of
DNSServiceRegister(...)
- context
- see documentation of
DNSServiceRegister(...)
Event: ‘error’
function onError(exception) {}
Emitted on asynchronous errors.
ad.start()
Start the advertisement.
ad.stop()
Stop the advertisement.
mdns.Browser
A mdns.Browser
performs the discovery part. It emits events as services appear and disappear on the network. For new services it also resolves host name, port and IP addresses. The resolver sequence is fully user configurable.
Services are reported for each interface they are reachable on. Partly because that is what dns_sd is doing, partly because anything else would mean making assumptions.
new mdns.Browser(serviceType, [options])
Create a new browser to discover services that match the given serviceType
. options
may contain the following properties:
- resolverSequence
- custom resolver sequence for this browser
- interfaceIndex
- one-based index of the network interface the services should be discovered on. Deprecated: Use networkInterface instead.
- networkInterface
- the network interface to use. See Network Interfaces for details.
- domain
- see documentation of
DNSServiceBrowse(...)
- context
- see documentation of
DNSServiceBrowse(...)
- flags
- see documentation of
DNSServiceBrowse(...)
Event: ‘serviceUp’
function onServiceUp(service) {}
Emitted when a new matching service is discovered.
Event: ‘serviceDown’
function onServiceDown(service) {}
Emitted when a matching service disappears.
Event: ‘serviceChanged’
function onServiceChanged(service) {}
Emitted when a matching service either appears or disappears. It is a new service if service.flags
has mdns.kDNSServiceFlagsAdd
set.
Event: ‘error’
function onError(exception) {}
Emitted on asynchronous errors.
browser.start()
Start the browser.
browser.stop()
Stop the browser.
mdns.Browser.defaultResolverSequence
This is the resolver sequence used by all browser objects that do not override it. It contains three steps. On platforms that have DNSServiceGetAddrInfo(...)
it has the following items:
var default_sequence = [
mdns.rst.DNSServiceResolve()
, mdns.rst.DNSServiceGetAddrInfo()
, mdns.rst.makeAddressesUnique()
];
On platforms that don’t, mdns.rst.getaddrinfo(...)
is used instead. You could modify the default sequence but you shouldn’t.
Resolver Sequence Tasks
mdns.rst.DNSServiceResolve(options)
Resolve host name and port. Probably all but the empty sequence start with this task. The options
object may have the following properties:
- flags
- flags passed to
DNSServiceResolve(...)
mdns.rst.DNSServiceGetAddrInfo(options)
Resolve IP addresses using DNSServiceGetAddrInfo(...)
mdns.rst.getaddrinfo(options)
Resolve IP addresses using nodes cares.getaddrinfo(...)
… but it’s a mess.
mdns.rst.makeAddressesUnique()
Filters the addresses to be unique.
mdns.rst.filterAddresses(f)
Filters the addresses by invoking f()
on each address. If f()
returns false the address is dropped.
mdns.rst.logService()
Print the service
object.
mdns.ServiceType
ServiceType
objects represent service type identifiers which have been discussed above. They store the required information in a normalized way and help with formating and parsing of these strings.
new mdns.ServiceType(…)
Construct a ServiceType
object. When called with one argument the argument may be
- a service type identifier (string)
- an array, the first element being the type, the second the protocol. Additional items are
subtypes
. - an object with properties
name
,protocol
and optionalsubtypes
All tokens may have a leading underscore. The n-ary form treats its arguments as an array. Copy construction works, too.
service_type.name
The primary service type.
service_type.protocol
The protocol used by the service. Must be ‘tcp’ or ‘udp’.
service_type.subtypes
Array of subtypes.
service_type.toString()
Convert the object to a service type identifier.
service_type.fromString(string)
Parse a service type identifier and store the values.
service_type.toArray()
Returns the service type in array form.
service_type.fromArray(array)
Set values from an array.
service_type.fromJSON(obj)
Set values from object, including other ServiceType
objects.
Functions
mdns.tcp(…)
Expressive way to create a ServiceType
with protocol tcp.
mdns.udp(…)
Expressive way to create a ServiceType
with protocol udp.
mdns.makeServiceType(…)
Constructs a ServiceType
from its arguments. If the first and only argument is a ServiceType
it is just returned.
mdns.createBrowser(serviceType, [options])
This factory function constructs a Browser
.
mdns.createAdvertisement(serviceType, port, [options], [callback])
This factory function constructs an Advertisement
.
mdns.resolve(service, [sequence], callback)
Fill in a service
object by running a resolver sequence
. If no sequence is given the Browser.defaultResolverSequence
is used. The callback has the signature (error, service)
.
mdns.browseThemAll(options)
Creates a browser initialized with the wildcard service type. When started the browser emits events for each service type instead of each service instance. The service
objects have no name
property. By default the browser has an empty resolver sequence. You still can set one using the options
object.
mdns.loopbackInterface()
Returns the platform and version dependent constant to set up a browser or advertisement for local operation. See Network Interfaces for details.
Constants
All dns_sd constants (supported by the implementation) are exposed on the mdns
object. Refer to the dns_sd API documentation for a list.
mdns.isAvahi
A boolean that is true when running on avahi. It’s a kludge though.
mdns.dns_sd
mdns.dns_sd
contains the native functions and data structures. The functions are bound to javascript using the exact C names and arguments. This breaks with the usual node convention of lower-case function names.
Design Notes
The implementation has two layers: A low-level API and a more user friendly object based API. The low-level API is implemented in C++ and just wraps functions, data structures and constants from dns_sd.h
. Most of the code deals with argument conversion and error handling. A smaller portion deals with callbacks from C(++) to javascript.
The high-level API is written in javascript. It connects the low-level API to nodes non-blocking IO infrastructure.
Compatibility Notes
TBD.