Skip to content

Latest commit

 

History

History
488 lines (301 loc) · 24.4 KB

user_guide.ejs

File metadata and controls

488 lines (301 loc) · 24.4 KB

<% title(‘User Guide’) %>

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:

  1. 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.
  2. 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.
  3. 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.
lookupInterfaceNames
when false the interface name will not be resolved.
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 optional subtypes

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.

Further Reading