Using AIR 2.0 to find services through mDNS/Zeroconf (Bonjour)
I am not a networking guru so there is a lot of room for improvement on this idea. What I wanted to do was use the new AIR 2.0 DatagramSocket feature to try and find an IP printer on my home network. I wanted to do this with no knowledge of the printers ip address or my local computers ip address for that matter. I didn’t want to try and guess on ip addresses or loop over an ip block. This is where Zeroconf and mDNS, aka Bonjour, comes into play.
First let me describe the context of the technology, remember I am no expert. There are two main methods of finding services on a network. The two approaches (not surprisingly) are from two camps, one from Microsoft called Simple Service Discovery Protocol (SSDP), a UPnP protocol, and the other from Apple called Bonjour. The basic difference is SSDP uses HTTP header filled packets where Bonjour uses DNS resource records, they both use multicast network capabilities. There are other differences and plenty of quirks on both sides that I will not even try and discuss here. Where this blog post will focus on is only trying to find a service on the network using mDNS. It will not try and register or help in link-local address autoconfiguration, maybe in the future.
What is mDNS? Apple created the mDNSResponder classes and made the code available for others to use. The executable is known as mDNS. Bonjour uses mDNS and DNS based Service Discovery (DNS-SD), since Mac OS X 10.2. It handles all the DNS resource records used to communicate over the multicast address 188.8.131.52:5353. These resource records are made up of a Query (aka Question), Answer, Authority Answer, and Additional Records. You send off a request using the DNS-SD service types, which is a bit funky. Because the DNS-SD and mDNS is overloading the DNS resource record types for local service lookup with out a real DNS server they have their own format for domains. For now on when I say service request I mean a URI that represents a service type. First all service requests have to end in “.local”, this can play an issue on some network configurations but thats really a Bonjour and network configuration issue. Its a mechanism for your computer to route “.local” domains to Bonjour instead of to a DNS server. If the local computer is setup because of network topology to have “.local” domains to point to real DNS servers then the issue appears. The rest of the parts of the mDNS server request domain are
_<service>._<protocol>.local, ie: “_ipp._tcp.local” would request all IP printers. There are few services that you might not really realize use this, for example iTunes sharing is the service type: “_home-sharing._tcp.local”. A link and more info of DNS-SD service types can be found at the bottom of the post.
To test what we are going to do, and a great debugging method, is to bring up a terminal on Mac and try out mDNS. In the terminal do:
>mDNS -B _ipp._tcp. .
Browsing for _ipp._tcp.
Timestamp A/R Flags Domain Service Type Instance Name
11:30:16.752 Add 0 local. _ipp._tcp. Brother HL-2070N series
Here you see that on my local machine I have a “Brother HL-2070N series” printer. If you know how to use Wireshark you can check out the packets going back and forther on the 184.108.40.206:5353 to see what the DNS Resource Records look like, this is how I debugged my code. Now for some code!
AIR 2.0 Multicast and the Solution
AIR 2.0 does not currently support multicast directly as an API, but for my idea it is lucky that the RFC for mDNS requires a fallback of sorts. If a DNS query is sent to 220.127.116.11:5353 with a source port that is not 5353 it requires all devices that would respond with an answer to answer the source ip address and source non-5353 port directly, instead of on the multicast 18.104.22.168:5353 ip and port. Here is what the AIR and Flex code looks like to send out a DNS resource record to the mDNS ip and port, and then listen for responses. This is where AIR 2.0 DatagramSocket comes into play. Here is the code snippet of just the relevant DatagramSocket code, you’ll find a link to all the source at the end of the post.
private var udpSocket:DatagramSocket;
//* Standard mDNS port and addresses for Bounjour
private var mDNSPort:int = 5353;
private var mDNSAddress:String = "22.214.171.124";
private function init():void
udpSocket = new DatagramSocket();
// ... Event Handlers ...
* Send standard DNS requests.
private function sendQuery():void
// Don't bind until first message sent out and only bind once
if (udpSocket.localAddress == null)
var bytes:ByteArray = new ByteArray();
// Write DNS Resource Record Query
// Send DNS packet to mDNS port and address, since we are sending from a
// non-5353 port it will send the response back as a unicast DNS answer
udpSocket.send(bytes, 0, 0, mDNSAddress, mDNSPort);
<s:TextInput width="240" id="service" text="_ipp._tcp" />
<s:Button label="Resolve" click="sendQuery()" />
Some things to be aware of when using DatagramSocket class. When you use the bind() method with no ip or port the system binds to all the local interfaces, ie: ip of *.* and finds the next available port, usually something high like 59018. You are not listening for data until you call the receive() method. Note, since the DatagramSocket is connecting to all interfaces and a random port you might want to do some packet filtering on the data event handler. This works fine for the purpose of this code but you might need more restrictions for your networking application needs. The send() method sends of the DNS resource record with DNS query as a packed BtyeArray. That is how simple the DatagramSocket class is to setup send the requests and listen for the return DNS resource records.
Now the fun is writing the DNS resource records query packets and decoding the resulting answers in ActionScript. The DNS resource record format is found in the RFC and I provide some basic classes that took care of parsing the DNS packets. Here is code showing how the BtyeArray gets packed with a DNS resource record and adds a query of “_ipp._tcp” in the proper place.
// DNS Header
bytes.writeByte(0); // First 5 bytes, Transaction ID: 0x0000, Query, Opcode: 0x000 etc..
bytes.writeByte(1); // 1 Question
bytes.writeByte(0); // No Answer RRs
bytes.writeByte(0); // No Authority RRs
bytes.writeFloat(0); // No Additional RRs, and 3 bytes of 0s
// Query Name Chunk
// Write out Type and Class (PTR/12) and 00 01
bytes.writeByte(0); // 00
bytes.writeByte(12); // 0C
bytes.writeByte(0); // 00
bytes.writeByte(1); // 01
var query:String = "_ipp.tcp";
var arr:Array = query.split(".");
var len:int = arr.length;
for (var i:int = 0; i < len; i++)
bytes.writeByte((arr[i] as String).length);
// Add .local and 00
For the decoding of the DNS response I created a couple of classes that you’ll find in the source project. Here is an example of how those classes are used:
trace("DATA: " + event.data.length);
var answersLength:int = dataMessage.answers.length;
trace("Answer Resources: " + answersLength);
for (var i:int = 0; i < answersLength; i++)
if (dataMessage.answers[i].type == DNSResourceRecord.TYPE_PTR)
trace("Answer Data: " + dataMessage.answers[i].data);
var additionalLength:int = dataMessage.additionals.length;
for (var j:int = 0; j < additionalLength; j++)
if (dataMessage.additionals[j].type == DNSResourceRecord.TYPE_SRV)
trace("Answer Port: " + dataMessage.additionals[j].data.port);
if (dataMessage.additionals[j].type == DNSResourceRecord.TYPE_A)
trace("Answer Address: " + dataMessage.additionals[j].data);
Now this code works fine with the assumption that only one answer will be in the returned DNS resource record. This might not always be the case but since the mDNS forces the devices to talk back to my local computer directly its pretty sure that it will only hold data from the that device. In the case of my local IP printer it returns a message with one Answer record stating its PTR record which just provides its domain name, but it provides A, TXT, and SRV records as Additional records which is where I found the ip address and port for the IP printer. In the case where multiple answers are returned you will have to use the Answer record’s data to correlate the Additional records to the data you are looking for.
This blog post is quite long but there is a lot still not covered. Please grab the source and give it a try. Don’t just try out “_ipp._tcp” but other services like iTunes Home Sharing. Any comments and additions to the code are welcome, everything is licensed as MIT so have fun.
Code and More Information:
Source Code in the form of a Flash Builder project (just change .fxp to .zip if you want to get at the files directly).
RFC for Domain Names or DNS resource records – http://www.ietf.org/rfc/rfc1035.txt
List of DNS-SD service names. Note this is not offical and people could write their own. This is an attempt to list known service types. – http://www.dns-sd.org/ServiceTypes.html