Using AIR 2.0 to find services through mDNS/Zeroconf (Bonjour)

Posted on March 29, 2010 | 12 comments

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.

Zeroconf/mDNS Context

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 224.0.0.251: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 224.0.0.251: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 224.0.0.251: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 224.0.0.251: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.

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                       xmlns:s="library://ns.adobe.com/flex/spark"
                       xmlns:mx="library://ns.adobe.com/flex/mx"
                       applicationComplete="init()">
    <s:layout>
        <s:VerticalLayout />
    </s:layout>
    <fx:Script>
        <![CDATA[          
            private var udpSocket:DatagramSocket;
           
            //* Standard mDNS port and addresses for Bounjour
            private var mDNSPort:int = 5353;
            private var mDNSAddress:String = "224.0.0.251";
           
            private function init():void
            {
                udpSocket = new DatagramSocket();
                udpSocket.addEventListener(DatagramSocketDataEvent.DATA, dataHandler);
                udpSocket.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
            }
            // ... Event Handlers ...

            /**
             *  Send standard DNS requests.
             *     http://www.ietf.org/rfc/rfc1035.txt
             */

            private function sendQuery():void
            {
                // Don't bind until first message sent out and only bind once
                if (udpSocket.localAddress == null)
                {
                    udpSocket.bind(); //
                    udpSocket.receive();           
                }              
               
                var bytes:ByteArray = new ByteArray();
                // Write DNS Resource Record Query
                // bytes.write.....
               
                // 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);
            }
        ]]>
        </fx:Script>
    <s:HGroup>
        <s:TextInput width="240" id="service" text="_ipp._tcp" />
        <s:Button label="Resolve" click="sendQuery()" />
    </s:HGroup>
</s:WindowedApplication>

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.

var bytes:ByteArray = new ByteArray();

// DNS Header
bytes.writeFloat(0);
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
writeQueryName(bytes, service.text);

// 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);
    bytes.writeUTFBytes(arr[i]);
}
// Add .local and 00
bytes.writeByte(5);
bytes.writeUTFBytes("local");
bytes.writeByte(0);

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:

private function dataHandler(event:DatagramSocketDataEvent):void
{
    trace("DATA: " + event.data.length);
    dataMessage.parse(event.data);
    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

  • http://blog.kevinhoyt.org Kevin Hoyt

    Bonjour?! Really?! Show-off!

    Welcome to the team,
    Kevin

  • mc

    Great post! We did something similar. The issue we ran into is with certain client firewalls. Since the request goes out the multicast network no unicast responses are going to be able to punch through the client’s firewall (since there was no outgoing request to that specific host). The firewall just drops them because they look like out-of-band UDP packets. Wonder if there are other ideas….

    • http://www.renaun.com Renaun Erickson

      Firewalls are definitely an issue for a lot of these types of things. I think the best solution would be get full multicast support into AIR.

  • http://www.codfusion.com John Mason

    What do we need to do to have multicast support in AIR 2? What is the logic to restrict that functionality?

    • http://www.renaun.com Renaun Erickson

      Mostly resourcing and testing the feature out. Please submit ideas/feature request at http://ideas.adobe.com/

  • Master P

    How would you use this example to be able to broadcast a service?

    • Anonymous

      You will have to use AIR 3.0 and Native Extensions

      • Master P

        Thanks for the quick response! Would you be able to provide any insight on how I would utilize this new feature?

        • Anonymous

          Native extensions would allow to take the mDNS, Bonjour, or UPnP libraries written in the various languages and code all DNS low level stuff with them. Then u hook that back into ActionscriptScript with this new feature in AIR 3.0.

          • Master P

            Alright so looked into native extensions and got something sort of worked out for a starting point but there doesnt seen to be much in the way of documentation (for OS X so I just adapted an example for iOS) everything works but for some reason XCode 4.1 throws:

            Undefined symbols for architecture i386:
            “_FREDispatchStatusEventAsync”, referenced from:
            _ContextInitializer in Unicast.o
            “_FRENewObjectFromBool”, referenced from:
            _IsSupported in Unicast.o
            ld: symbol(s) not found for architecture i386

            Can’t find any info on this and have tried several things any ideas?

          • Anonymous

            I haven’t done Mac OS desktop Native Extensions yet so not sure. Bless place to try is the Adobe forums for AIR.

  • Arun289

    Thanks! I needed to discover the IP address of a custom service running on the LAN from an AIR app. Works great!