Just An Application

November 22, 2013

Service Discovery In Android And iOS: Part Nine – iOS Take Five

And if none of the previous choices are to your liking then there is also the theoretical possibility of doing the whole thing from scratch as in the Java case.

For this to be feasible it must be possible to send and receive multicast UDP datagrams at the application level. There is nothing in Foundation or Core Foundation that supports this directly but it is possible at the POSIX level.

1.0 Opening A Multicast UDP Socket Using POSIX System Calls

Opening a multicast UDP socket at the POSIX level is feasible if a little bit fiddly as befits something that needs to be done in C/C++.

It requires three system calls.

1.1 socket

First we need to open a socket using the socket system call which is declared in the header file <sys/socket.h>
like this

    int socket(int domain, int type, int protocol);

In our case the domain argument is AF_INET because we want to use the socket to communicate using IP over the network.

The type argument is SOCK_DGRAM because we want to use the socket to send and receive datagrams.

The protocol argument is 0 which means we want to use the default protocol given the socket domain and type which should be UDP.

So our code to open the socket looks like this.

    ...
    
    s = socket(AF_INET, SOCK_DGRAM, 0);
    if (s == -1)
    {
        perror("socket");
        return -1;
    }

    ...

1.2 bind

Having opened the socket we need to associate it with a local address and port using the bind system call which is declared in the header file <sys/socket.h> like this

    int bind(int socket, const struct sockaddr *address, socklen_t address_len);

In our case we need to specify an IP address using an instance of struct sockaddr_in which is defined in the header file
<netinet/in.h> and then cast appropriately.

On the assumption that there is only one network interface so we do not need to be bound to a specific one we specify the address as INADDR_ANY.

As we do not need to send and receive on a specific port we specify the port as 0 so the system will allocate a port number for us.

    ...
    
    struct sockaddr_in inetAddress;
    
    memset(&inetAddress, 0, sizeof(inetAddress));
    
    inetAddress.sin_family      = AF_INET;
    inetAddress.sin_addr.s_addr = htonl(INADDR_ANY);
    inetAddress.sin_port        = htons(0);
    
    
    if (bind(s, (const struct sockaddr*)&inetAddress, sizeof(inetAddress)) == -1)
    {
        perror("bind");
        close(s);
        return -1;
    }

    ...

Note

The address and port in the sockaddr_in struct should be in network byte order hence the use of htonl and htons.

As it happens in this case it is not necessary to convert either the address or port from host to network byte order but doing so in all cases helps ensure that you don’t forget in a case where it is necessary.

1.3 setsockopt

Having bound the socket to a local address and port number we need to associate it with the multicast group we wish to send to and receive from.

We do this using the setsockopt system call which is defined like this in the header file <sys/socket.h>

    int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);

In our case level is IPPROTO_IP as we are setting an IP level option.

The option_name is IP_ADD_MEMBERSHIP indicating that we want to add the socket to the membership of a multicast group.

The option_value is a pointer to an instance of the struct ip_mreq which specifies the group address,
imr_multiaddr, and the network interface, imr_interface.

    ...

    struct ip_mreq imr;
    
    imr.imr_multiaddr.s_addr = group;
    imr.imr_interface.s_addr = htonl(INADDR_ANY);
    
    if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr)) == -1)
    {
        perror("setsockopt");
        close(s);
        return -1;
    }
    
    ...

where group is the multicast group address in network byte order.

As before we assume there is only a single network interface and specify INADDR_ANY.

Note

The imr_multiaddr and imr_interface fields are both of type struct in_addr and as in the bind case above the addresses must be in network byte order.

2.0 Making A POSIX Socket More iOS Like

Putting to one side for the moment the issue of whether it is actually possible to open a multicast socket there is the question of how it could be used in the context of an iOS application.

Although a POSIX socket is perfectly functional, receiving datagrams using the recvfrom system call is by default synchronous so using it as is would require at least one separate non-standard thread.

Fortunately the function CFSocketCreateWithNative can turn a theoretical POSIX socket into a theoretical CFSocket.

Using CFSocketCreateWithNative we can specify a function to be called whenever the socket is readable and in return we will get a CFSocketRef

Once we have a theoretical CFSocketRef we can obtain a CFRunLoopSource using the function CFSocketCreateRunLoopSource and then add it to the CFRunLoop of our choice.

The end result will be that our function is called when the socket is readable so recvfrom will not block and this will be done in the context of a standard CFRunLoop.

3.0 But Does It Work ?

Perhaps surprisingly the answer is yes, at least on an iPad running iOS 7.0.4.

DNS messages can be multicast, responses received and services duly discovered.

4.0 The MulticastSocket Class

Here is an initial attempt at encapsulating the multicast socket.

    //
    //  MulticastSocket.m
    //  XperTakeFive
    //
    //  Created by Simon Lewis on 03/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    
    #import "MulticastSocket.h"
    
    @interface MulticastSocket()
    
    - (void)read;
    
    @end
    
    @implementation MulticastSocket
    {
        CFSocketRef                 socketRef;
        CFRunLoopSourceRef          runLoopSourceRef;
    
        in_addr_t                   group;
        int                         s;
    
        struct sockaddr_in          from;
        unsigned char               buffer[4096];
    }
    
    static void socketCallback(
                    CFSocketRef          theSocketRef,
                    CFSocketCallBackType theCallbackType,
                    CFDataRef            theAddress,
                    const void*          theData,
                    void*                theInfo)
    {
        switch (theCallbackType)
        {
            case kCFSocketReadCallBack:
    
                [(__bridge MulticastSocket*)theInfo read];
                break;
    
            default:
    
                NSLog(@"socketCallback: %lu !", theCallbackType);
        }
    }
    
    - (id)init:(NSString*)theGroup
    {
        self = [super init];
        if (self != nil)
        {
            group = inet_addr([theGroup UTF8String]);
        }
        return self;
    }
    
    - (void)dealloc
    {
        if (s != -1)
        {
            close(s);
            CFRelease(socketRef);
            CFRelease(runLoopSourceRef);
        }
    }
    
    - (BOOL)open
    {
        s = socket(AF_INET, SOCK_DGRAM, 0);
        if (s == -1)
        {
            perror("socket");
            return NO;
        }
    
        struct sockaddr_in inetAddress;
    
        memset(&inetAddress, 0, sizeof(inetAddress));
    
        inetAddress.sin_family      = AF_INET;
        inetAddress.sin_addr.s_addr = htonl(INADDR_ANY);
        inetAddress.sin_port        = htons(0);
    
    
        if (bind(s, (const struct sockaddr*)&inetAddress, sizeof(inetAddress)) == -1)
        {
            perror("bind");
            return NO;
        }
    
        struct ip_mreq imr;
    
        imr.imr_multiaddr.s_addr = group; // in network byte order courtesy of inet_addr
        imr.imr_interface.s_addr = htonl(INADDR_ANY);
    
        if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr)) == -1)
        {
            perror("setsockopt");
            return NO;
        }
    
        CFSocketContext context;
    
        memset(&context, 0, sizeof(context));
    
        context.info = (__bridge void*)self;
    
        socketRef = CFSocketCreateWithNative(NULL, s, kCFSocketReadCallBack, socketCallback, &context);
        if (socketRef == NULL)
        {
            return NO;
        }
        runLoopSourceRef = CFSocketCreateRunLoopSource( NULL, socketRef, 0);
        if (runLoopSourceRef == NULL)
        {
            return NO;
        }
        CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSourceRef, kCFRunLoopDefaultMode);
        return YES;
    }
    
    - (void)send:(unsigned char*)theData ofLength:(ssize_t)theLength toPort:(uint16_t)thePort
    {
        struct sockaddr_in inetAddress;
    
        memset(&inetAddress, 0, sizeof(inetAddress));
    
        inetAddress.sin_family      = AF_INET;
        inetAddress.sin_addr.s_addr = group;
        inetAddress.sin_port        = ntohs(thePort);
    
        ssize_t nBytes = sendto(s, theData, theLength, 0, (const struct sockaddr*)&inetAddress, sizeof(inetAddress));
    
        if (nBytes == -1)
        {
            perror("sendto");
        }
    }
    
    // internal
    
    - (void)read
    {
        socklen_t length = sizeof(from);
    
        memset(&from, 0, sizeof(from));
    
        ssize_t nBytes = recvfrom(s, buffer, sizeof(buffer), 0, (struct sockaddr*)&from, &length);
    
        if (nBytes != -1)
        {
            [self.delegate received:buffer ofLength:nBytes from:&from];
        }
        else
        {
            perror("recvfrom");
        }
    }
    
    @end

5.0 And The Rest

The rest is just reading and writing messages which is much the same in Objective-C as it is in Java.

For example, here is the method for reading a message.

    ...
    
    - (BOOL)readRecord:(Category)theCategory
    {
        NSString* name = [self readNodeName];
    
        if (name == nil)
        {
            return NO;
        }
        if ((offset + HEADER_LENGTH) > length)
        {
            return NO;
        }
    
        uint16_t type     = UNSIGNED_SHORT;
        uint16_t class    = UNSIGNED_SHORT;
        uint32_t ttl      = UNSIGNED_INT;
        uint16_t rdlength = UNSIGNED_SHORT;
    
        [responseHandler
             record:
                 [[ResourceRecord alloc]
                       init:
                           name
                       type:
                           type
                       class:
                           class
                       ttl:
                           ttl
                       data:
                           [[RecordData alloc]
                                 init:
                                     message
                                 offset:
                                     offset
                                 length:
                                     offset + rdlength]]
             category:
                 theCategory];
        offset += rdlength;
        return YES;
    }

    ...

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 20, 2013

Service Discovery In Android And iOS: Part Eight – iOS Take Four

If DNSServiceBrowse and friends are not to your liking there is always the function DNSServiceQueryRecord which enables us to obtain DNS records directly.

1.0 DNSServiceQueryRecord

The DNSServiceQueryRecord function declaration follows the common function pattern and looks like this

    DNSServiceErrorType DNSServiceQueryRecord(
                            DNSServiceRef*             sdRef,
                            DNSServiceFlags            flags,
                            uint32_t                   interfaceIndex,
                            const char*                fullname,
                            uint16_t                   rrtype,
                            uint16_t                   rrclass,
                            DNSServiceQueryRecordReply callBack,
                            void*                      context);

The fullname should be the absolute name of the node for which the record or records are being requested.

The rrtype should be the type of the record or records being requested.

The rrclass should be the class of the record or records being requested.

If the kDNSServiceFlagsTimeout bit is set in the flags argument then the function will timeout after a system dependent amount of time.

2.0 The DNSServiceQueryRecord Callback Function

The callback function will be invoked

  • once for each record that is received in response to the query

  • once for each record that is received in response to the query that subsequently expires

  • if an error occurs

  • if a timeout occurs

3.0 DNSServiceQueryRecordReply

The DNSServiceQueryRecordReply function type declaration follows the common function type pattern and looks like this

    typedef void (*DNSServiceQueryRecordReply)(
                       DNSServiceRef       sdRef,
                       DNSServiceFlags     flags,
                       uint32_t            interfaceIndex,
                       DNSServiceErrorType errorCode,
                       const char*         fullname,
                       uint16_t            rrtype,
                       uint16_t            rrclass,
                       uint16_t            rdlen,
                       const void*         rdata,
                       uint32_t            ttl,
                       void*               context);

When a function of this type is invoked, then, if the errorCode argument is kDNSServiceErr_NoError

  • fullname is the absolute name of the node with the record is associated

  • rrtype is the type of the record

  • rrclass is the class of the record.

  • rdlen is the length of the record data

  • rdate is the record data

  • ttl is the time in seconds for which the record is valid

The flags argument will have the kDNSServiceFlagsAdd bit set if the callback is being invoked when a record is received in response to the query.

If kDNSServiceFlagsAdd bit is clear then callback is being invoked because the record has expired,
in which case the ttl argument will be 0.

If a timeout occurs the value of the errorCode argument will be kDNSServiceErr_Timeout.

3.0 The Query Class

We can encapsulate the call to DNSServiceQueryRecord and its associated callback function in a class
like so

    //
    //  Query.m
    //  XperTakeFour
    //
    //  Created by Simon Lewis on 08/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
        
    #import <dns_sd.h>
        
    #import "QueryDelegate.h"
    #import "Record.h"
        
    #import "Query.h"
        
    @interface Query()
        
    @property NSString* name;
    @property uint16_t  type;
    @property uint16_t class;
        
    - (void)record:(const Record*)theRecord onInterface:(uint32_t)theIndex;
        
    - (void)recordExpired:(const Record*)theRecord;
        
    - (void)failed:(DNSServiceErrorType)theName;
        
    @end
        
    @implementation Query
    {
        DNSServiceRef   ref;
    }
        
    - (Query*)init:(NSString*)theName type:(uint16_t)theType class:(uint16_t)theClass
    {
        self = [super init];
        if (self != nil)
        {
            self.name  = theName;
            self.type  = theType;
            self.class = theClass;
        }
        return self;
    }
    
    static void queryCallback(
                    DNSServiceRef       sdRef,
                    DNSServiceFlags     theFlags,
                    uint32_t            theInterfaceIndex,
                    DNSServiceErrorType theErrorCode,
                    const char*         theName,
                    uint16_t            theType,
                    uint16_t            theClass,
                    uint16_t            theDataLength,
                    const void*         theData,
                    uint32_t           theTTL,
                    void*               theContext)
    {
        NSLog(@"queryCallback: flags == %d error code == %d", theFlags, theErrorCode);
        
        if (theErrorCode != kDNSServiceErr_NoError)
        {
            [(__bridge Query*)theContext failed:theErrorCode];
        }
        else
        {
            NSLog(@"theName == %s theType == %u", theName, theType);
        
            Record rr = {
                            theName,
                            theType,
                            theClass,
                            theTTL,
                            theDataLength,
                            theData
                        };
        
            if ((theFlags & kDNSServiceFlagsAdd) != 0)
            {
                [(__bridge Query*)theContext record:&rr onInterface:theInterfaceIndex];
            }
            else
            {
                [(__bridge Query*)theContext recordExpired:&rr];
            }
        }
    }
        
        
    - (void)start:(DNSServiceRef)theServiceRef interface:(uint32_t)theInterfaceIndex timeout:(BOOL)timeout
    {
        ref = theServiceRef;
        
        DNSServiceErrorType error;
        DNSServiceFlags     flags;
        
        flags = kDNSServiceFlagsShareConnection;
        if (timeout)
        {
            flags |= kDNSServiceFlagsTimeout;
        }
        error = DNSServiceQueryRecord(
                    &ref,
                    flags,
                    theInterfaceIndex,
                    [self.name UTF8String],
                    self.type,
                    self.class,
                    queryCallback,
                    (__bridge void*)self);
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"DNSServiceQueryRecord: %d", error);
            [self.delegate queryDidFail:self withError:error];
        }
    }
        
    - (void)record:(const Record*)theRecord onInterface:(uint32_t)theIndex
    {
        [self.delegate query:self didGetResponse:theRecord onInterface:theIndex];
    }
        
    - (void)recordExpired:(const Record *)theRecord
    {
        [self.delegate query:self recordDidExpire:theRecord];
    }
        
    - (void)failed:(DNSServiceErrorType)theErrorCode
    {
        if (theErrorCode != kDNSServiceErr_Timeout)
        {
            [self.delegate queryDidFail:self withError:theErrorCode];
        }
        else
        {
            [self.delegate queryDidTimeout:self];
        }
    }
        
    @end

4.0 Using The Query Class

The start method of FindServices v4 starts the search by querying for PTR records associated with the service type node.

    ...
    
    - (BOOL)start
    {
        DNSServiceErrorType error = DNSServiceCreateConnection(&dnsServiceRef);
        
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"Error: DNSServiceCreateConnection %d", error);
            return NO;
        }
        error = DNSServiceSetDispatchQueue(dnsServiceRef, dispatch_get_main_queue());
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"Error: DNSServiceSetDispatchQueue %d", error);
            return NO;
        }
        self.ptrQuery = [[Query alloc] init:self.type type: kDNSServiceType_PTR class:kDNSServiceClass_IN];
        self.ptrQuery.delegate = self;
        [self.ptrQuery start:dnsServiceRef interface:kDNSServiceInterfaceIndexAny timeout:NO];
        return YES;
    }
    
    ...

5.0 Examples

I will forgo the output from the examples as there is only so many console log messages anybody can be expected to find interesting.

Needless to say everything works in pretty much the same way as it did in the three preceding incarnations of the
FindServices class.


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 18, 2013

Service Discovery In Android And iOS: Part Seven – iOS Take Three: Down At C Level

For anyone who considers both Foundation and Core Foundation too rarified there is always a C function API for performing DNS/mDNS based service discovery and registration.

This is declared in the header file

    /usr/include/dns_sd.h

1.0 DNSServiceRefs And Connection Sharing

By default each function in the API which requires DNS functionality establishes a separate connection to the DNS service which it returns as a DNSServiceRef.

In this context the name DNSServiceRef can be a bit confusing.

A DNSServiceRef is a reference to the DNS Service, that is, the thing doing the search for services using DNS, not a reference to a service found using DNS.

There is an alternative to having a per function call connection and DNSServiceRef.

A connection to the DNS service can be established separately and then shared by passing it to each function that would otherwise create a new connection.

2.0 The Function Pattern

All the functions in the API which access the DNS service are declared using a common pattern

    DNSServiceErrorType <function-name>(
                            DNSServiceRef*           sdRef,
                            DNSServiceFlags          flags,
                            uint32_t                 interfaceIndex,
        
                            ... <function specific arguments> ...
    
                            <function specific type> callBack,
                            void*                    context);

2.1 The sdRef Argument

    DNSServiceRef*  sdRef

The sdRef argument is either

  • a pointer to an uninitalized DNSServiceRef which will be initialized to a valid DNSServiceRef if the call succeeds, or

  • a pointer to a shared DNSServiceRef which should be used by the function

2.2 The flags argument

    DNSServiceFlags flags

The flags argument unsurprisingly specifies zero or more flags. If an initialized DNSServiceRef is being passed via the sdRef argument then the flag

    kDNSServiceFlagsShareConnection

must be set.

2.3 The interfaceIndex Argument

    uint32_t   interfaceIndex

The index of the network interface to use to perform the requested DNS operation(s).

If the specific network interface is not important then when starting the search for services the constant

    kDNSServiceInterfaceIndexAny

can be used.

When a service is found the index of the network interface it is associated with is reported and subsequent calls can use this value.

2.4 The callBack Argument

The type of the callBack argument is specific to the function to which it is being passed but in each case it specifies the function to be invoked when a a result is available or an error occurs.

2.5 The info Argument

    void*  context

The value of the context argument will be passed as an argument to the callback function specified by
the callBack argument.

3.0 The Callback Function Pattern

All the callback function types are declared using a common pattern.

    typedef void (*<type-name>)(
                        DNSServiceRef       sdRef,
                        DNSServiceFlags     flags,
                        uint32_t            interfaceIndex,
                        DNSServiceErrorType errorCode,
    
                        ... <function specific arguments> ...

                        void*               context);

3.1 The sdRef Argument

The DNSServiceRef which was passed to the function which invoked this callback.

3.2 The flags Argument

The flags argument is used to pass general status information, e.g., if the

    kDNSServiceFlagsMoreComing

flag is set then this callback will be invoked again.

3.3 The interfaceIndex Argument

The index of the network interface on which the result was obtained.

3.4 The errorCode Argument

If the errorCode argument is not

    kDNSServiceErr_NoError

then an error has occurred.

3.5 The context Argument

The value of the context argument passed to the function which invoked this callback.

4.0 Searching For Services

4.1 DNSServiceBrowse

We can start the search for services of a given type by using the function DNSServiceBrowse
which is declared like this

    DNSServiceErrorType DNSServiceBrowse(
                            DNSServiceRef*        sdRef,
                            DNSServiceFlags       flags,
                            uint32_t              interfaceIndex,
                            const char*           regtype,
                            const char*           domain,
                            DNSServiceBrowseReply callBack,
                            void*                 context);

The regtype argument should be the domain relative type name, e.g.,

    "_ipp.tcp."

The domain argument should be the absolute name of the domain to search in, e.g.,

    "local."

4.2 The DNSServiceBrowse Callback Function

The function passed as the callBack argument to DNSServiceBrowse will be called once for each service of the given type that is found.

If the information about a service that was found becomes invalid, implying that it has ‘disappeared’, then the callback function will called again.’

4.3 The DNSServiceBrowseReply Function Type

The function type DNSServiceBrowseReply is declared like this

    typedef void (*DNSServiceBrowseReply)(
                       DNSServiceRef       sdRef,
                       DNSServiceFlags     flags,
                       uint32_t            interfaceIndex,
                       DNSServiceErrorType errorCode,
                       const char*         serviceName,
                       const char*         regtype,
                       const char*         replyDomain,
                       void*               context);

When a function of this type is invoked, then if the errorCode argument is kDNSServiceErr_NoError

  • serviceName is the type relative name of the service.

  • regtype is the domain relative name of the service type

  • replyDomainis the absolute name of the domain the service is registered in

The kDNSServiceFlagsAdd flag will be set in the flags argument if the service has been found, and clear if the service has been ‘lost’.

5.0 Resolving Services

Resolving a service involves two functions

    DNSServiceResolve

which obtains the service’s SRV and TXT records, and

and

    DNSServiceGetAddrInfo.

which obtains the address or addresses of the host on which the service is running

5.1 DNSServiceResolve

We can obtain the information contained in the SRV and TXT records associated with a given service by using the function
DNSServiceResolve which is declared like this

    DNSServiceErrorType DNSServiceResolve(
                            DNSServiceRef*         sdRef,
                            DNSServiceFlags        flags,
                            uint32_t               interfaceIndex,
                            const char*            name,
                            const char*            regtype,
                            const char*            domain,
                            DNSServiceResolveReply callBack,
                            void*                  context);

The name argument should be the type relative name of the service.

The regtype argument should be the domain relative name of the service type.

The domain argument should be the absolute name of the domain in which the service is registered.

5.1.1 The DNSServiceResolve Callback Function

The function passed as the callBack argument to DNSServiceResolve will be called once, either with the results of with an error.

5.1.1 The DNSServiceResolveReply Function Type

The function type DNSServiceResolveReply is declared like this

    typedef void (*DNSServiceResolveReply)(
                       DNSServiceRef        sdRef,
                       DNSServiceFlags      flags,
                       uint32_t             interfaceIndex,
                       DNSServiceErrorType  errorCode,
                       const char*          fullname,
                       const char*          hosttarget,
                       uint16_t             port,
                       uint16_t             txtLen,
                       const unsigned char* txtRecord,
                       void*                context);

When a function of this type is invoked, then, if the errorCode argument is kDNSServiceErr_NoError

  • fullname is the absolute name of the service, e.g., "ipp_server_1._ipp._tcp.local.".

  • hosttarget is the name of the host the service is running on.

  • port is the port, in network byte order, the service is listening on.

  • txtLen is is the length of the TXT record data.

  • txtRecord is a pointer to the TXT record data itself.

5.2 DNSServiceGetAddrInfo

We can obtain the address of a host using the function DNSServiceGetAddrInfo which is declared like this

    DNSServiceErrorType DNSServiceGetAddrInfo(
                            DNSServiceRef*             sdRef,
                            DNSServiceFlags            flags,
                            uint32_t                   interfaceIndex,
                            DNSServiceProtocol         protocol,
                            const char*                hostname,
                            DNSServiceGetAddrInfoReply callBack,
                            void*                      context);

If the

    kDNSServiceFlagsTimeout

is set in the flags argument then the operation may timeout after a system defined amount of time.

The protocol argument specifies the type of the address requested.

The value should be one of

  • 0

  • kDNSServiceProtocol_IPv4

  • kDNSServiceProtocol_IPv6

  • kDNSServiceProtocol_IPv4|kDNSServiceProtocol_IPv6

A value of 0 (zero) is usually equivalent to requesting both the IPv4 and IPv6 addresses.

The hostname argument should be the absolute name of the host.

5.2.1 The DNSServiceGetAddrInfo Callback Function

The function passed as the callBack argument to DNSServiceGetAddrInfo will be called once for each address type that was requested and is found

It will also be called if the address of the host becomes invalid, e.g., because the host has been turned off.

5.2.2 The DNSServiceGetAddrInfoReply Function Type

The function type DNSServiceGetAddrInfoReply is declared like this

    typedef void (*DNSServiceGetAddrInfoReply)(
                       DNSServiceRef          sdRef,
                       DNSServiceFlags        flags,
                       uint32_t               interfaceIndex,
                       DNSServiceErrorType    errorCode,
                       const char*            hostname,
                       const struct sockaddr* address,
                       uint32_t               ttl,
                       void*                  context);

When a function of this type is invoked, then, if the errorCode argument is kDNSServiceErr_NoError

  • hostname is the name of the host whose address this is

  • address is a pointer to its address, and

  • ttl is the time in seconds for which the given address is valid

The kDNSServiceFlagsAdd flag will be set in the flags argument if the address has been ‘found’, and clear if the address is no longer valid.

The kDNSServiceFlagsMoreComing will be set in the flags argument if there are more addresses, and will be clear if this is the last address.

The type of the address will of course depend upon what what specified as the protocol argument in the call to DNSServiceGetAddrInfo.

If both IPv4 and IPv6 addresses were requested then it will be necessary to examine the sa_family field of the sockaddr struct to find out which one it is.

6.0 The Care And Maintenance Of Your DNSServiceRef

Something that is not perhaps immediately apparent is that the shared DNSServiceRef or one created by a function like DNSServiceBrowse has to be actively handled on the client side.

There are two ways to do this, either

  • by obtaining the file descriptor associated with the connection by calling DNSServiceRefSockFD and doing it all yourself, or

  • by calling the function DNSServiceSetDispatchQueue which will result in the connection being handled ‘automatically’ on the dispatch queue of your choice.

6.1 DNSServiceRefSockFD

The function DNSServiceRefSockFD is declared like this

    int DNSSD_API DNSServiceRefSockFD(DNSServiceRef sdRef);

It takes a DNSServiceRef and returns the file descriptor of the underlying connection to the DNS service.

Once you have obtained your file descriptor you will need to determine when it is readable.

To do this you are going to need either

  • an fd_set and a system call, or

  • a pollfd struct and a different system call

6.1.1 Using select

To use the select system call and assuming sr is the DNSServiceRef you will need to do something like this.

    ...
    
    int fd = DNSServiceRefSockFD(sr);
    
    if (fd == -1)
    {
        fprintf(stderr, "fd == -1 !");
        return;
    }
    
    fd_set         readFDs;
    struct timeval tv;
    
    while (true)
    {
        FD_ZERO(&readFDs);
        FD_SET(fd, &readFDs);
    
        tv.tv_sec  = 1000000;
        tv.tv_usec = 0;
    
        int status = select(fd + 1, &readFDs, NULL, NULL, &tv);
    
        if (status == -1)
        {
            fprintf(stderr, "status == -1\n");
            break;
        }
        else
        if (status == 0)
        {
            fprintf(stderr, "status == 0\n");
        }
        else
        if (FD_ISSET(fd, &readFDs))
        {
            int error = DNSServiceProcessResult(sr);
    
            if (error != kDNSServiceErr_NoError)
            {
                fprintf(stderr, "DNSServiceProcessResult: error == %d\n", error);
                break;
            }
        }
    }

    ...
    

When the file descriptor is readable the function DNSServiceProcessResult is invoked to handle the input. It is this call that results in callback functions being invoked.

This assumes that you are sharing a single DNSServiceRef if not then you are going to end up knee deep in file descriptors and its all going to get very messy very fast.

6.1.2 Using poll

If the use of select is too retro for you you can always use the new-fangled poll system call.

The code looks very similar because poll is just select with unlimited [1] file descriptors.

    ...

    int fd = DNSServiceRefSockFD(sr);
    
    if (fd == -1)
    {
        NSLog(@"fd == -1 !");
        return;
    }
    
    struct pollfd   pollFD;
    
    while (true)
    {
        pollFD.fd = fd;
        pollFD.events = POLL_IN;
        
        int status = poll(&pollFD, 1 , 1000000);
    
        if (status == -1)
        {
            fprintf(stderr, "status == -1\n");
            break;
        }
        else
        if (status == 0)
        {
            fprintf(stderr, "status == 0\n");
        }
        else
        if ((pollFD.revents & POLL_IN) != 0)
        {
            int error = DNSServiceProcessResult(sr);

            if (error != kDNSServiceErr_NoError)
            {
                fprintf(stderr, "DNSServiceProcessResult: error == %d\n", error);
                break;
            }
        }
    }

    ...

6.2 DNSServiceSetDispatchQueue

The alternative to wrestling with file descriptors is the function DNSServiceSetDispatchQueue
which is declared like this

    DNSServiceErrorType DNSServiceSetDispatchQueue(
                            DNSServiceRef    service,
                            dispatch_queue_t queue);

The DNSServiceRef can be associated with a shared connection or with a per function connection.

See below for an example of its use.

7.0 Creating A DNSServiceRef For A Shared Connection

The only way to create a DNSServiceRef for a connection which can be shared is by using the function
DNSServiceCreateConnection which is declared like this

   DNSServiceErrorType DNSServiceCreateConnection(DNSServiceRef* sdRef);

A copy of the initialized DNSServiceRef that results should be passed to each function that is going to share the connection.

See below for an example of its use.

8.0 DNSServiceBrowse In Action

This is the start method of the third version of the FindServices class.

    - (BOOL)start
    {
        DNSServiceErrorType error = DNSServiceCreateConnection(&dnsServiceRef);
    
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"Error: DNSServiceCreateConnection %d", error);
            return NO;
        }
        error = DNSServiceSetDispatchQueue(dnsServiceRef, dispatch_get_main_queue());
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"Error: DNSServiceSetDispatchQueue %d", error);
            return NO;
        }
        browseRef = dnsServiceRef;
        error = DNSServiceBrowse(
                    &browseRef,
                    kDNSServiceFlagsShareConnection,
                    kDNSServiceInterfaceIndexAny,
                    [self.type UTF8String],
                    [self.domain UTF8String],
                    browseCallback,
                    (__bridge void*)self);
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"Error: DNSServiceBrowse %d", error);
            return NO;
        }
        return YES;
    }
    static void browseCallback(
                    DNSServiceRef       sdRef,
                    DNSServiceFlags     theFlags,
                    uint32_t            theInterfaceIndex,
                    DNSServiceErrorType theErrorCode,
                    const char*         theName,
                    const char*         theType,
                    const char*         theDomain,
                    void*               theContext)
    {
        NSLog(@"browseCallback:  error == %d flags == %s", theErrorCode, flagsToString(theFlags));
        
        if (theErrorCode == kDNSServiceErr_NoError)
        {
            ServiceIdentifier si = { theName, theType, theDomain };
        
            if ((theFlags & kDNSServiceFlagsAdd) != 0)
            {
                [(__bridge FindServices*)theContext serviceFound:&si onInterface:theInterfaceIndex];
            }
            else
            {
                [(__bridge FindServices*)theContext serviceLost:&si];
            }
        }
        else
        {
            [(__bridge FindServices*)theContext browseFailed:theErrorCode];
        }
    }

9.0 DNSServiceResolve In Action

This is the resolve:onInterface: method of the ServiceResolver class

    - (void)resolve:(ServiceIdentifier*)theServiceId onInterface:(uint32_t)theInterfaceIndex
    {
        DNSServiceErrorType error;
        
        error = DNSServiceResolve(
                    &resolveRef,
                    kDNSServiceFlagsShareConnection,
                    theInterfaceIndex,
                    theServiceId->name,
                    theServiceId->type,
                    theServiceId->domain,
                    resolveCallback,
                    (__bridge void*)self);
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"DNSServiceResolve: %d", error);
            [self.delegate serviceResolver:self didFail:error];
        }
    }

and this is the associated callback function.

    static void resolveCallback(
                    DNSServiceRef        theRef,
                    DNSServiceFlags      theFlags,
                    uint32_t             theInterfaceIndex,
                    DNSServiceErrorType  theErrorCode,
                    const char*          theFullName,
                    const char*          theTarget,
                    uint16_t             thePort,
                    uint16_t             theTXTRecordLength,
                    const unsigned char* theTXTRecord,
                    void*                theContext)
    {
        NSLog(@"resolveCallback: error == %d flags == %s", theErrorCode, flagsToString(theFlags));
        
        if (theErrorCode != kDNSServiceErr_NoError)
        {
            NSLog(@"resolveCallback: error !");
            [(__bridge ServiceResolver*)theContext resolveFailed:theErrorCode];
        }
        else
        if (theFlags == 0)
        {
            ServiceInfo si =
                {
                    theFullName,
                    theTarget,
                    ntohs(thePort),
                    theTXTRecordLength,
                    theTXTRecord
                };
        
            NSLog(@"%s %s %u", theFullName, theTarget, thePort);
        
            [(__bridge ServiceResolver*)theContext resolved:&si onInterface:theInterfaceIndex];
        }
        else
        {
            NSLog(@"resolveCallback: flags set !");
            [(__bridge ServiceResolver*)theContext internalError];
        }
    }

10.0 DNSServiceGetAddrInfo In Action

This is the resolved:onInterface: method of the ServiceResolver class

    - (void)resolved:(const ServiceInfo*)theServiceInfo onInterface:(uint32_t)theInterfaceIndex
    {
        DNSServiceErrorType error;
        
        error = DNSServiceGetAddrInfo(
                    &addressRef,
                    kDNSServiceFlagsShareConnection,
                    theInterfaceIndex,
                    kDNSServiceProtocol_IPv4|kDNSServiceProtocol_IPv6,
                    theServiceInfo->target,
                    addressInfoCallback,
                    (__bridge void*)self);
        if (error == kDNSServiceErr_NoError)
        {
            [self.builder serviceInfo:theServiceInfo];
        }
        else
        {
            NSLog(@"DNSServiceGetAddrInfo: %d", error);
            [self.delegate serviceResolver:self didFail:error];
        }
    }

and this is the associated callback function.

    static void addressInfoCallback(
                    DNSServiceRef          theServiceRef,
                    DNSServiceFlags        theFlags,
                    uint32_t               theInterfaceIndex,
                    DNSServiceErrorType    theErrorCode,
                    const char*            theHostname,
                    const struct sockaddr* theAddress,
                    uint32_t               theTTL,
                    void*                  theContext)
    {
        NSLog(@"addressInfoCallback: error == %d flags == %s ", theErrorCode, flagsToString(theFlags));
        
        if (theErrorCode != kDNSServiceErr_NoError)
        {
            NSLog(@"addressInfoCallback error");
            [(__bridge ServiceResolver*)theContext getAddrInfoFailed:theErrorCode];
        }
        else
        if ((theFlags & kDNSServiceFlagsAdd) != 0)
        {
            NSLog(@"theHostname == %s", theHostname);
            NSLog(@"theAddress->sa_family == %d", theAddress->sa_family);
        
            [(__bridge ServiceResolver*)theContext address:theAddress];
            if ((theFlags & kDNSServiceFlagsMoreComing) == 0)
            {
                [(__bridge ServiceResolver*)theContext done];
            }
        }
        else
        {
            NSLog(@"theHostname == %s", theHostname);
            NSLog(@"theAddress->sa_family == %d", theAddress->sa_family);
            // ignore
        }
    }

11.0 Examples

In each case FindServices is looking for services of type

    "_ipp._tcp."

in the domain

    "local."

In each case the log output is from FindServices and its delegate running on an iPad running iOS 7.0.

7.1 A Single IPPServer

A single instance of the CUPS test server IPPServer with the name ipp_server_1
running on a Mac and then being stopped.

In this case the addressInfoCallback function is called twice, first with the IPv6 address then with the IPv4 address.

    ...
        
    2013-11-18 14:53:20.304 XperTakeThree[334:60b] browseCallback:  error == 0 flags == kDNSServiceFlagsAdd
    2013-11-18 14:53:20.307 XperTakeThree[334:60b] serviceFound: ipp_server_1._ipp._tcp.local.
    2013-11-18 14:53:20.309 XperTakeThree[334:60b] resolveCallback: error == 0 flags == <none>
    2013-11-18 14:53:20.310 XperTakeThree[334:60b] ipp_server_1._ipp._tcp.local. Simons-Computer.local. 56088
    2013-11-18 14:53:20.312 XperTakeThree[334:60b] addressInfoCallback: error == 0 flags == kDNSServiceFlags{Add,MoreComing}
    2013-11-18 14:53:20.313 XperTakeThree[334:60b] theHostname == Simons-Computer.local.
    2013-11-18 14:53:20.314 XperTakeThree[334:60b] theAddress->sa_family == 30
    2013-11-18 14:53:20.315 XperTakeThree[334:60b] addressInfoCallback: error == 0 flags == kDNSServiceFlagsAdd
    2013-11-18 14:53:20.316 XperTakeThree[334:60b] theHostname == Simons-Computer.local.
    2013-11-18 14:53:20.317 XperTakeThree[334:60b] theAddress->sa_family == 2
    2013-11-18 14:54:18.545 XperTakeThree[334:60b] browseCallback:  error == 0 flags == <none>
    2013-11-18 14:54:18.547 XperTakeThree[334:60b] serviceLost: name == ipp_server_1._ipp._tcp.local.
        
    ...

11.2 A Single Printer

A printer being turned on and then turned off a couple of minutes later.

In this case we only get a single address, the IPV4 one, but we do get a second call to the function addressInfoCallback
function when the printer is turned off

    ...
        
    2013-11-18 14:55:55.137 XperFS_DNS_SD[351:60b] browseCallback:  error == 0 flags == kDNSServiceFlagsAdd
    2013-11-18 14:55:55.140 XperFS_DNS_SD[351:60b] serviceFound: Canon MG6200 series._ipp._tcp.local.
    2013-11-18 14:55:55.141 XperFS_DNS_SD[351:60b] resolveCallback: error == 0 flags == <none>
    2013-11-18 14:55:55.142 XperFS_DNS_SD[351:60b] Canon32MG620032series._ipp._tcp.local. 7D300C000000.local. 30466
    2013-11-18 14:55:55.144 XperFS_DNS_SD[351:60b] addressInfoCallback: error == 0 flags == kDNSServiceFlagsAdd
    2013-11-18 14:55:55.145 XperFS_DNS_SD[351:60b] theHostname == 7D300C000000.local.
    2013-11-18 14:55:55.146 XperFS_DNS_SD[351:60b] theAddress->sa_family == 2
    2013-11-18 15:02:18.835 XperFS_DNS_SD[351:60b] addressInfoCallback: error == 0 flags == <none>
    2013-11-18 15:02:18.837 XperFS_DNS_SD[351:60b] theHostname == 7D300C000000.local.
    2013-11-18 15:02:18.839 XperFS_DNS_SD[351:60b] theAddress->sa_family == 2
    2013-11-18 15:02:19.936 XperFS_DNS_SD[351:60b] browseCallback:  error == 0 flags == <none>
    2013-11-18 15:02:19.938 XperFS_DNS_SD[351:60b] serviceLost: name == Canon MG6200 series._ipp._tcp.local.

    ...

Notes

  • Subject to terms and conditions. The number of file descriptors may be subject to limits.

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 14, 2013

Service Discovery In Android And iOS: Part Six – iOS Take Two

At the Core Foundation level we have the CFNetServices API which supports the registration and discovery of services using DNS/mDNS.

1.0 CFNetServiceBrowser

A CFNetServiceBrowser is the CFNetServices equivalent of an NSNetServiceBrowser.

2.0 CFNetService

A CFNetService is the CFNetServices equivalent of an NSNetService.

3.0 Synchronous vs. Aynchronous

Unlike the NSNetServiceBrowser and NSNetService methods the equivalent CFNetServiceBrowser and CFNetService functions can be used synchronously or asynchronously.

By default the functions are synchronous.

To make them asynchronous for a given CFNetServiceBrowser or CFNetService it must be added to a CFRunLoop.

4.0 Searching For Services Asynchronously

To search for services of a given type using the CFNetworkServices API we need to

  1. create a CFNetServiceBrowser

  2. make it asynchronous

  3. start the search

4.1 CFNetServiceBrowserCreate

We can create a CFNetServiceBrowser using the function CFNetServiceBrowserCreate which is declared like this

    CFNetServiceBrowserRef CFNetServiceBrowserCreate (
                               CFAllocatorRef                    alloc,
                               CFNetServiceBrowserClientCallBack clientCB,
                               CFNetServiceClientContext*        clientContext);

The alloc argument is a reference to an allocator to use when creating the CFNetServiceBrowser or more likely the constant

    kCFAllocatorDefault

4.1.1 The clientCB Argument

The clientCB argument is a pointer to a function of type CFNetServiceBrowserClientCallBack.

This function will be invoked when a service is found or if an error occurs.

The function type CFNetServiceBrowserClientCallBack is declared like this

    typedef void (*CFNetServiceBrowserClientCallBack) (
                       CFNetServiceBrowserRef browser,
                       CFOptionFlags          flags,
                       CFTypeRef              domainOrService,
                       CFStreamError*         error,
                       void*                  info);;

The browser argument is the CFNetServiceBrowserRef that was returned from the call to CFNetServiceBrowserCreate.

The callback function is also invoked when searching for domains so the flags argument is used to distinguish between the two uses.

It is also used to distinguish between the discovery of a service and the disappearance of a previously discovered service.

When a service is discovered the value of the flags argument will be zero (0).

When a previously discovered service disappears the value of the flags argument will be

   kCFNetServiceFlagRemove

When being invoked on the discovery or disappearance of a service the domainOrService argument will be a
CFNetServiceRef.

If an error has occurred, the error and domain fields of error argument will be set.

The info argument is the value of the info field of the CFNetServiceClientContext passed to the call to CFNetServiceBrowserCreate.

4.1.2 The clientContext Argument

The clientContext argument is a pointer to a CFNetServiceClientContext which is declared like this.

    struct CFNetServiceClientContext {
        CFIndex                            version;
        void*                              info;
        CFAllocatorRetainCallBack          retain;
        CFAllocatorReleaseCallBack         release;
        CFAllocatorCopyDescriptionCallBack copyDescription;
    };
        
    typedef struct CFNetServiceClientContext CFNetServiceClientContext;

The info field can be used to store a pointer to an ‘object’ which will be passed to the callback function passed via the clientCB argument.

If the retain and release fields are not NULL then the functions specified will be invoked when the implementation wishes to retain and release the ‘object’ specified in the info field.

It is not clear from the documentation whether the contents of the CFNetServiceClientContext are copied.

A const qualifier is sometimes a clue, but a little experimentation shows that they are, so it is safe to stack allocate.

4.2 CFNetServiceBrowserScheduleWithRunLoop

We can add our newly created CFNetServiceBrowser to a CFRunLoop using the function CFNetServiceBrowserScheduleWithRunLoop which is declared like this

    void CFNetServiceBrowserScheduleWithRunLoop(
             CFNetServiceBrowserRef browser,
             CFRunLoopRef           runLoop,
             CFStringRef            runLoopMode);

The easiest way to use this is to add the CFNetServiceBrowser to the main CFRunLoop in the default mode like so

    CFNetServiceBrowserScheduleWithRunLoop(
        browser, 
        CFRunLoopGetMain(), 
        kCFRunLoopDefaultMode);

4.3 CFNetServiceBrowserSearchForServices

Once we have made our CFNetServiceBrowse asynchronous we can start the search for services by calling the function CFNetServiceBrowserSearchForServices which is declared like this

    Boolean CFNetServiceBrowserSearchForServices (
                CFNetServiceBrowserRef browser,
                CFStringRef            domain,
                CFStringRef            serviceType,
                CFStreamError*         error);

The browser argument should be the CFNetServiceBrowserRef returned from the call to CFNetServiceBrowserCreate.

The domain argument should be the absolute name of the domain in which to search, e.g.,

    "local."

The serviceType argument should be the domain relative type of the services to search for, e.g.,

    "_ipp._tcp."

The documentation for the error argument is a tad confusing.

It states

A pointer to a CFStreamError structure, that, if an error occurs, will be set to the error and the error’s domain and passed to your callback function.

The

… and passed to your callback

bit does not appear to be true.

If it is not possible to start the search then the function returns false immediately and the domain and error fields of the CFStreamError are set.

5.0 Resolving A Service Asynchronously

Once a service has been found we need to resolve it …

To do this asynchronously we need to

  1. make it possible to get the result asynchronously

  2. make the CFNetService asynchronous

  3. start the resolution

5.1 CFNetServiceSetClient

To get the results from CFNetService functions when running asynchronously we must associate a callback function and a context with the CFNetService first.

We do this using the function CFNetServiceSetClient which is declared like this

    Boolean CFNetServiceSetClient(
                CFNetServiceRef            theService,
                CFNetServiceClientCallBack clientCB,
                CFNetServiceClientContext* clientContext);

5.1.1 The clientCB Argument

The clientCB argument is a pointer to a function of type CFNetServiceClientCallBack

This function will be invoked when the service is resolved or an error occurs.

The function type CFNetServiceClientCallBack is declared like this

    typedef void (*CFNetServiceClientCallBack) (
                      CFNetServiceRef theService,
                      CFStreamError*  error,
                      void*           info);

The info argument is the value of the info field of the CFNetServiceClientContext passed as the clientContext argument in the call to the CFNetServiceSetClient

If an error has occurred, the error and domain fields of error argument will be set.

5.1.2 The clientContext Argument

The clientContext argument is a pointer to a CFNetServiceClientContext which we have already seen used with the function CFNetServiceBrowserCreate.

5.2 CFNetServiceScheduleWithRunLoop

We can add our CFNetService instance to a CFRunLoop using the function CFNetServiceScheduleWithRunLoop which is declared like this.

    void CFNetServiceScheduleWithRunLoop(
             CFNetServiceRef theService,
             CFRunLoopRef    runLoop,
             CFStringRef     runLoopMode);

The easiest way to use this is to add the CFNetService to the main CFRunLoop in the default mode like so

    CFNetServiceScheduleWithRunLoop(service, CFRunLoopGetMain(), kCFRunLoopDefaultMode);

5.3 CFNetServiceResolveWithTimeout

Once we have made our CFNetService asynchronous we can start the resolution bey calling the function
CFNetServiceResolveWithTimeout which is declared like this

    Boolean CFNetServiceResolveWithTimeout(
                CFNetServiceRef theService,
                CFTimeInterval  timeout,
                CFStreamError*  error);

The timeout argument specifies the amount of time in seconds that the implementation should wait for the resolution to complete If the value is less than or equal to zero the implementation will wait indefinitely.

If it not possible to start the resolution the function returns false immediately and the domain and error fields of the CFStreamError argument are set.

6.0 The FindServices Class

Here is the FindServices class re-written to use the CFNetServices API.

    //
    //  FindServices.m
    //  XperTakeTwo
    //
    //  Created by Simon Lewis on 12/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
    
    #import "Service.h"
    
    #import "FindServices.h"
    
    @interface FindServices ()
    
    @property NSString*             type;
    @property NSString*             domain;
    
    @property NSMutableDictionary*  services;
    
    
    - (void)serviceFound:(CFNetServiceRef)theService;
    
    - (void)serviceLost:(CFNetServiceRef)theService;
    
    
    - (void)resolved:(CFNetServiceRef)theService;
    
    - (void)resolveFailed:(CFNetServiceRef)theService withError:(CFStreamError*)theError;
    
    
    - (void)stopBrowser;
    
    - (void)stopService:(CFNetServiceRef)theService;
    
    - (void)log:(NSString*)theMessage service:(CFNetServiceRef)theService;
    
    @end
    
    @implementation FindServices
    {
        CFNetServiceBrowserRef      browser;
    }
    
    - (FindServices*)initWithType:(NSString*)theType andDomain:(NSString*)theDomain
    {
        self = [super init];
        if (self != nil)
        {
            self.type     = theType;
            self.domain   = theDomain;
            self.services = [NSMutableDictionary dictionaryWithCapacity:8];
        }
        return self;
    }
    
    
    
    static void browserCallBack(
                    CFNetServiceBrowserRef  theBrowser,
                    CFOptionFlags           theFlags,
                    CFTypeRef               theDomainOrService,
                    CFStreamError*          theError,
                    void*                   theInfo)
    {
        NSLog(@"browserCallBack");
    
        if ((theError->error) != 0)
        {
            NSLog(@"error: %d\n", (int)theError->error);
        }
        else
        if ((theFlags & kCFNetServiceFlagIsDomain) != 0)
        {
            NSLog(@"domain !\n");
        }
        else // service
        if ((theFlags & kCFNetServiceFlagRemove) == 0)
        {
            [(__bridge FindServices*)theInfo serviceFound:(CFNetServiceRef)theDomainOrService];
        }
        else
        {
            [(__bridge FindServices*)theInfo serviceLost:(CFNetServiceRef)theDomainOrService];
        }
    }
    
    - (void)start
    {
        CFNetServiceClientContext   context;
    
        memset(&context, 0, sizeof(context));
        context.info = (__bridge void *)(self);
    
        browser = CFNetServiceBrowserCreate(kCFAllocatorDefault, browserCallBack, &context);
    
        CFNetServiceBrowserScheduleWithRunLoop(browser, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
    
        Boolean       status;
        CFStreamError error;
    
        status = CFNetServiceBrowserSearchForServices(
                     browser,
                     (__bridge CFStringRef)self.domain,
                     (__bridge CFStringRef)self.type,
                     &error);
        if (status == 0)
        {
            NSLog(@"error.error == %d\n", (int)error.error);
            [self stopBrowser];
        }
    }

    static void resolveCallBack(
                    CFNetServiceRef theService,
                    CFStreamError*  theError,
                    void*           theInfo)
    {
        NSLog(@"resolveCallback");

        if (theError->error == 0)
        {
            [(__bridge FindServices*)theInfo resolved:theService];
        }
        else
        {
            [(__bridge FindServices*)theInfo resolveFailed:theService withError:theError];
        }
    }
    
    - (void)serviceFound:(CFNetServiceRef)theService
    {
        [self log:@"service found: %@.%@%@" service:theService];
    
        CFNetServiceClientContext   context;
    
        memset(&context, 0, sizeof(context));
        context.info = (__bridge void *)(self);
    
        CFNetServiceSetClient(theService, resolveCallBack, &context);
    
        CFNetServiceScheduleWithRunLoop(theService, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
    
        Boolean       status;
        CFStreamError error;
    
        status = CFNetServiceResolveWithTimeout(theService, 0.0, &error);
        if (status == 0)
        {
            NSLog(@"error.error == %d\n", (int)error.error);
    
           [self stopService:theService];
        }
    }
    
    - (void)serviceLost:(CFNetServiceRef)theService
    {
        [self log: @"service lost: %@.%@%@" service:theService];
    
        NSString* name   = [NSString stringWithFormat:
                                @"%@.%@%@",
                                CFNetServiceGetName(
                                    theService),
                                CFNetServiceGetType(
                                    theService),
                                CFNetServiceGetDomain(
                                    theService)];
        Service* service = [self.services objectForKey:name];
    
        if (service != nil)
        {
            [service lost];
            [self.services removeObjectForKey:name];
            [self.delegate findServices:self didLoseService:service];
        }
        else
        {
            [self log: @"service lost but was not found: %@.%@%@" service:theService];
        }
    }
    
    - (void)resolved:(CFNetServiceRef)theService
    {
        [self log:@"service resolved: %@.%@%@" service:theService];
    
        CFArrayRef addresses = CFNetServiceGetAddressing(theService);
    
        if (CFArrayGetCount(addresses) != 0)
        {
            Service* service = [[Service alloc] init:theService];
    
            [self.services setObject:service forKey:service.name];
            [self.delegate findServices:self didFindService:service];
        }
        else
        {
            NSLog(@"service has 0 addresses: lost ?");
        }
    }
    
    - (void)resolveFailed:(CFNetServiceRef)theService withError:(CFStreamError *)theError
    {
        [self log:@"service resolvedFailed: %@.%@%@" service:theService];
        [self stopService:theService];
    }
    
    - (void)stopBrowser
    {
        CFNetServiceBrowserUnscheduleFromRunLoop(browser, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
        CFNetServiceBrowserInvalidate(browser);
        CFNetServiceBrowserStopSearch(browser, NULL);
    }
    
    - (void)stopService:(CFNetServiceRef)theService
    {
        CFNetServiceClientContext   context;
    
        memset(&context, 0, sizeof(context));
    
        CFNetServiceUnscheduleFromRunLoop(theService, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
        CFNetServiceSetClient(theService, NULL, &context);
        CFNetServiceCancel(theService);
    }
    
    - (void)log:(NSString*)theMessage service:(CFNetServiceRef)theService
    {
        NSLog(
            theMessage,
            CFNetServiceGetName(
                theService),
            CFNetServiceGetType(
                theService),
           CFNetServiceGetDomain(
               theService));
    }
    
    @end

7.0 Examples

In each case FindServices is looking for services of type

    "_ipp._tcp."

in the domain

    "local."

In each case the log output is from FindServices and its delegate running on an iPad running iOS 7.0.

7.1 A Single IPPServer

A single instance of the CUPS test server IPPServer with the name ipp_server_1 running on a Mac and then being stopped.

    ...

    2013-11-14 14:07:07.561 XperTakeTwo[665:60b] browserCallBack
    2013-11-14 14:07:07.564 XperTakeTwo[665:60b] service found: ipp_server_1._ipp._tcp.local.
    2013-11-14 14:07:07.593 XperTakeTwo[665:60b] resolveCallback
    2013-11-14 14:07:07.595 XperTakeTwo[665:60b] service resolved: ipp_server_1._ipp._tcp.local.
    2013-11-14 14:07:07.597 XperTakeTwo[665:60b] findServices:didFindService:<Service: 0x14e86990>
    2013-11-14 14:07:13.931 XperTakeTwo[665:60b] browserCallBack
    2013-11-14 14:07:13.933 XperTakeTwo[665:60b] service lost: ipp_server_1._ipp._tcp.local.
    2013-11-14 14:07:13.935 XperTakeTwo[665:60b] findServices:didLoseService::<Service: 0x14e86990>

    ...

7.2 A Single Printer

A printer being turned on and then turned a couple of minutes later.

Note the lines shown in bold for emphasis.

When the printer is turned off the resolveCallback function is being called for a second time but this time the CFNetService has no addresses.

This is the same thing that happened in this case when using the NSNetServiceBrowser/NSNetService API. (see here)

    ...

    2013-11-14 14:07:55.177 XperTakeTwo[671:60b] browserCallBack
    2013-11-14 14:07:55.180 XperTakeTwo[671:60b] service found: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:07:55.250 XperTakeTwo[671:60b] resolveCallback
    2013-11-14 14:07:55.252 XperTakeTwo[671:60b] service resolved: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:07:55.255 XperTakeTwo[671:60b] findServices:didFindService:<Service: 0x1452e060>

    2013-11-14 14:09:05.838 XperTakeTwo[671:60b] resolveCallback
    2013-11-14 14:09:05.840 XperTakeTwo[671:60b] service resolved: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:09:05.843 XperTakeTwo[671:60b] service has 0 addresses: lost ?

    2013-11-14 14:09:06.910 XperTakeTwo[671:60b] browserCallBack
    2013-11-14 14:09:06.912 XperTakeTwo[671:60b] service lost: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:09:06.915 XperTakeTwo[671:60b] findServices:didLoseService::<Service: 0x1452e060>

    ...

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 12, 2013

Service Discovery In Android And iOS: Part Five – iOS Take One

When it comes to iOS we are spoilt for choice. There are no less than three APIs available for service discovery.

Starting at the Foundation level we have the Objective-C class NSNetServiceBrowser.

1.0 Instance Creation

The NSNetServiceBrowser class defines a single no argument init method so to create one we simply do this

    browser = [[NSNetServiceBrowser alloc] init];

2.0 Starting A Search

The search for services of a given type is asynchronous.

To start the search we call the method

    - (void)searchForServicesOfType:(NSString*)type inDomain:(NSString*)domainString;

The type argument should be the domain relative name of the service type to search for, e.g.,

    "_ipp._tcp."

Note the dot (‘.’) at the end.

The domainString should be the absolute name of the domain to search, e.g.,

    "local."

Note the dot (‘.’) at the end.

Once the search has started it will continue indefinitely unless it is explicitly stopped.

3.0 NSNetServiceBrowserDelegate

NSNetServiceBrowser uses the standard delegate pattern to return its results.

The delegate is required to implement the NSNetServiceBrowserDelegate protocol.

3.1 netServiceBrowserWillSearch:

Following the call to the method searchForServicesOfType:inDomain: if the NSNetServiceBrowser instance is going to perform the search it calls the delegate’s implementation of the method

    - (void)netServiceBrowserWillSearch:(NSNetServiceBrowser*)netServiceBrowser

3.2 netServiceBrowser:didNotSearch:

Following the call to the method searchForServicesOfType:inDomain: if the NSNetServiceBrowser instance is not going to perform the search it calls the delegate’s implementation of the method

    - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didNotSearch:(NSDictionary*)errorInfo

The easiest way to see this method in action is to get the type name wrong in the call to searchForServicesOfType:inDomain:, e.g.,

    "._ipp.tcp."

in which case the NSDictionary passed as the errorInfo argument will look like this

    {
        NSNetServicesErrorCode   = "-72004";
        NSNetServicesErrorDomain = 10;
    }

You can find the error codes in NSNetServices.h.

3.3 netServiceBrowser:didFindService:moreComing:

Following the call to the method searchForServicesOfType:inDomain: if the NSNetServiceBrowser instance finds a service of the given type it callsthe delegate’s implementation of the method

    - (void)netServiceBrowser:
                (NSNetServiceBrowser*)netServiceBrowser
            didFindService:
                (NSNetService*)netService
            moreComing:
                (BOOL)moreServicesComing

3.4 netServiceBrowser:didRemoveService:moreComing:

If a service that has previously been found is no longer available the NSNetServiceBrowser instance
calls the delegate’s implementation of the method

    - (void)netServiceBrowser:
                (NSNetServiceBrowser*)netServiceBrowser
            didRemoveService:
                (NSNetService*)netService
            moreComing:
                (BOOL)moreServicesComing

4.0 NSNetService

As discovered, the service as represented by the NSNetService instance is still in a nascent state.
Neither its address nor the key/value pairs in its TXT record are available.

To be useful it has to be resolved.

The resolution of a service is performed asynchronously. It is started by invoking the NSNetService
method resolveWithTimeout: which is declared like this

    - (void)resolveWithTimeout:(NSTimeInterval)timeout

Once started the resolution of a service will either complete successfully ot time out after the interval specified by the
timeout argument.

5.0 NSNetServiceDelegate

The success or failure of the resolution of a NSNetService instance is reported to the delegate of that instance.

The delegate must implement the NSNetServiceDelegate protocol.

5.1 netServiceWillResolve:

Following the call to the resolveWithTimeout: method the NSNetService instance will call its delegate’s implementation of the method

    - (void)netServiceWillResolve:(NSNetService*)sender

5.2 netService:didNotResolve:

If the resolution of the service fails the NSNetService instance calls its delegate’s implementation of the method

    - (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict

5.3 netServiceDidResolveAddress:

If the resolution of the service succeeds the NSNetService instance calls its delegate’s implementation of the method

    - (void)netServiceDidResolveAddress:(NSNetService*)sender;t

There is a caveat however. As we shall see ‘success’ in this context does not always mean what you might expect.’

6.0 The FindServices Class

To make things slightly simpler we can wrap up the classes and their delegates in a single class FindServices
as we did in the Java case.

FindServicesDelegate.h

    //
    //  FindServicesDelegate.h
    //  XperTakeOne
    //
    //  Created by Simon Lewis on 03/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
        
    #import <Foundation/Foundation.h>
        
    @class FindServices;
    @class Service;
        
    @protocol FindServicesDelegate <NSObject>
        
    - (void)findServices:(FindServices*)theFindServices didFindService:(Service*)theService;
        
    - (void)findServices:(FindServices*)theFindServices didLoseService:(Service*)theService;
        
    @end

FindServices.h

    //
    //  FindServices.h
    //  XperTakeOne
    //
    //  Created by Simon Lewis on 03/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
        
    #import <Foundation/Foundation.h>
        
    #import "FindServicesDelegate.h"
        
    @interface FindServices : NSObject<NSNetServiceBrowserDelegate, NSNetServiceDelegate>
        
    @property (weak) id<FindServicesDelegate>   delegate;
        
    - (FindServices*)initWithType:(NSString*)theType andDomain:(NSString*)theDomain;
        
    - (void)start;
        
    @end

FindServices.m

    //
    //  FindServices.m
    //  XperTakeOne
    //
    //  Created by Simon Lewis on 03/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
        
    #import "Service.h"
        
    #import "FindServices.h"
        
    @interface FindServices ()
        
    @property NSString*             type;
    @property NSString*             domain;
    @property NSNetServiceBrowser*  browser;
    @property NSMutableArray*       resolving;
    @property NSMutableDictionary*  services;
        
    @end
        
    @implementation FindServices
        
    - (FindServices*)initWithType:(NSString*)theType andDomain:(NSString*)theDomain
    {
        self = [super init];
        if (self != nil)
        {
            self.type      = theType;
            self.domain    = theDomain;
            self.browser   = [[NSNetServiceBrowser alloc] init];
            self.resolving = [NSMutableArray arrayWithCapacity:8];
            self.services  = [NSMutableDictionary dictionaryWithCapacity:8];
        
            self.browser.delegate = self;
        }
        return self;
    }
        
    - (void)start
    {
        [self.browser searchForServicesOfType:self.type inDomain:self.domain];
    }
        
    // NSNetServiceBrowserDelegate
        
    - (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)theBrowser
    {
        NSLog(@"netServiceBrowserWillSearch:\n");
    }
        
    - (void)netServiceBrowser:(NSNetServiceBrowser *)theBrowser didNotSearch:(NSDictionary *)theErrors
    {
        NSLog(@"netServiceBrowser:didNotSearch: %@", theErrors);
    }
        
    - (void)netServiceBrowser:
                (NSNetServiceBrowser *)aNetServiceBrowser
            didFindService:
                (NSNetService *)theService
            moreComing:
                (BOOL)moreComing
    {
        NSLog(@"netServiceBrowser:didFindService: %@", theService);
        
        [self.resolving addObject:theService];
        theService.delegate = self;
        [theService resolveWithTimeout:0.0];
    }
        
    - (void)netServiceBrowser:
                (NSNetServiceBrowser *)aNetServiceBrowser
            didRemoveService:
                (NSNetService *)theService
            moreComing:
                (BOOL)moreComing
    {
        NSLog(@"netServiceBrowser:didRemoveService: %@", theService);
        
        Service* service = [self.services objectForKey:theService.name];
        
        if (service != nil)
        {
            [self.services removeObjectForKey:theService.name];
            [self.delegate findServices:self didLoseService:service];
        }
        else
        {
            NSLog(@"%@ removed without being found ?", theService.name);
        }
    }
        
    // NSNetServiceDelegate
        
    - (void)netServiceWillResolve:(NSNetService *)theService
    {
        NSLog(@"netServiceWillResolve");
    }
        
    - (void)netServiceDidResolveAddress:(NSNetService *)theService
    {
        NSUInteger nAddresses = [[theService addresses] count];
        
        NSLog(@"netServiceDidResolveAddress: %@ nAddresses == %lu", theService, (unsigned long)nAddresses);
        
        if (nAddresses != 0)
        {
            Service* service = [[Service alloc] init:theService];
        
            [self.resolving removeObject:theService];
            [self.services setObject:service forKey:theService.name];
            [self.delegate findServices:self didFindService:service];
        }
        else
        {
            Service* service = [self.services objectForKey:theService.name];
        
            if (service != nil)
            {
                NSLog(@"service %@ now has 0 addresses !", theService.name);
            }
            else
            {
                NSLog(@"resolve failed ? %@ has 0 addresses", theService.name);
            }
        }
    }
        
    - (void)netService:(NSNetService *)theService didNotResolve:(NSDictionary *)theErrors
    {
        NSLog(@"netServiced:didNotResolve: %@ %@", theService, theErrors);
        
        [self.resolving removeObject:theService];
    }
        
    @end

7.0 Examples

In each case FindServices is looking for services of type

    "_ipp._tcp."

in the domain

    "local."

In each case the log output is from FindServices and its delegate running on an iPad running iOS 7.0.

7.1 A Single IPPServer

A single instance of the CUPS test server IPPServer with the name ipp_server_1 running on a Mac and then being stopped.

    ...

    2013-11-12 08:26:05.516 XperTakeOne[163:60b] netServiceBrowserWillSearch:
    2013-11-12 08:26:12.500 XperTakeOne[163:60b] netServiceBrowser:didFindService: \
        <NSNetService 0x1464bd60> local. _ipp._tcp. ipp_server_1
    2013-11-12 08:26:12.504 XperTakeOne[163:60b] netServiceWillResolve
    2013-11-12 08:26:12.533 XperTakeOne[163:60b] netServiceDidResolveAddress: \
        <NSNetService 0x1464bd60> local. _ipp._tcp. ipp_server_1 nAddresses == 2
    2013-11-12 08:26:12.535 XperTakeOne[163:60b] findServices:didFindService: <Service: 0x14631890>
    2013-11-12 08:27:56.204 XperTakeOne[163:60b] netServiceBrowser:didRemoveService: \
        <NSNetService 0x14522f50> local. _ipp._tcp. ipp_server_1
    2013-11-12 08:27:56.207 XperTakeOne[163:60b] findServices:didLoseService: <Service: 0x14631890>

    ...

7.2 Two IPPServers

Two instances of the CUPS test server IPPServer with the names ipp_server_1 and ipp_server_2 running on a Mac and then being stopped.

    ...

    2013-11-12 08:29:54.404 XperTakeOne[171:60b] netServiceBrowserWillSearch:
    2013-11-12 08:29:54.525 XperTakeOne[171:60b] netServiceBrowser:didFindService: \
        <NSNetService 0x1757ac10> local. _ipp._tcp. ipp_server_1
    2013-11-12 08:29:54.528 XperTakeOne[171:60b] netServiceWillResolve
    2013-11-12 08:29:54.531 XperTakeOne[171:60b] netServiceBrowser:didFindService: \
        <NSNetService 0x17578140> local. _ipp._tcp. ipp_server_2
    2013-11-12 08:29:54.533 XperTakeOne[171:60b] netServiceWillResolve
    2013-11-12 08:29:55.553 XperTakeOne[171:60b] netServiceDidResolveAddress:\
        <NSNetService 0x17578140> local. _ipp._tcp. ipp_server_2 nAddresses == 2
    2013-11-12 08:29:55.556 XperTakeOne[171:60b] findServices:didFindService: \
        <Service: 0x17673570>
    2013-11-12 08:29:55.558 XperTakeOne[171:60b] netServiceDidResolveAddress: \
        <NSNetService 0x1757ac10> local. _ipp._tcp. ipp_server_1 nAddresses == 2
    2013-11-12 08:29:55.559 XperTakeOne[171:60b] findServices:didFindService: <Service: 0x176b25f0>
    2013-11-12 08:30:57.454 XperTakeOne[171:60b] netServiceBrowser:didRemoveService: \
        <NSNetService 0x1757e7d0> local. _ipp._tcp. ipp_server_2
    2013-11-12 08:30:57.457 XperTakeOne[171:60b] findServices:didLoseService: <Service: 0x17673570>
    2013-11-12 08:31:02.678 XperTakeOne[171:60b] netServiceBrowser:didRemoveService: \
        <NSNetService 0x17581730> local. _ipp._tcp. ipp_server_1
    2013-11-12 08:31:02.680 XperTakeOne[171:60b] findServices:didLoseService: <Service: 0x176b25f0>
    
    ...

7.3 A Single Printer

A printer being turned on and then turned off five minutes later.

Note the lines shown in bold for emphasis.

When the printer is turned off the netServiceDidResolveAddress: method is being called for a second time but this time the NSNetService instance has no addresses.

This is the reason for the convoluted code in the FindServices implementation of the netServiceDidResolveAddress:.

Note also that this does not happen when an IPPServer instance is shutdown.

    ...

    2013-11-12 08:32:14.253 XperTakeOne[179:60b] netServiceBrowserWillSearch:
    2013-11-12 08:32:14.976 XperTakeOne[179:60b] netServiceBrowser:didFindService: \
        <NSNetService 0x146b3a50> local. _ipp._tcp. Canon MG6200 series
    2013-11-12 08:32:14.979 XperTakeOne[179:60b] netServiceWillResolve
    2013-11-12 08:32:15.008 XperTakeOne[179:60b] netServiceDidResolveAddress: \
        <NSNetService 0x146b3a50> local. _ipp._tcp. Canon MG6200 series nAddresses == 1
    2013-11-12 08:32:15.011 XperTakeOne[179:60b] findServices:didFindService: <Service: 0x14681460>

    2013-11-12 08:37:10.250 XperTakeOne[179:60b] netServiceDidResolveAddress: \
        <NSNetService 0x146b3a50> local. _ipp._tcp. Canon MG6200 series nAddresses == 0
    2013-11-12 08:37:10.252 XperTakeOne[179:60b] service Canon MG6200 series now has 0 addresses !

    2013-11-12 08:37:11.325 XperTakeOne[179:60b] netServiceBrowser:didRemoveService: \
        <NSNetService 0x146b21d0> local. _ipp._tcp. Canon MG6200 series
    2013-11-12 08:37:11.327 XperTakeOne[179:60b] findServices:didLoseService: <Service: 0x14681460>

    ...

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 3, 2013

Service Discovery In Android And iOS: Part Four – Doing It Properly In Java

Filed under: DNS, DNS Based Service Discovery, mDNS — Tags: , , , — Simon Lewis @ 8:37 pm

To do mDNS based service discovery in Java we would need to be able to

  • send and receive UDP multicast packets
  • read and write DNS messages

Fortunately both of theses things are eminently doable.

1.0 Multicasting

UDP multicasting we get for free courtesy of the class java.net.MulticastSocket.

1.1 Creating A Multicast Socket

Creating a multicast socket is just like creating a unicast socket but in addition we must call the method

    public void joinGroup(InetAddress mcastaddr)
                throws
                    IOException

to join the multicast group we wish to send to, e.g.,

    ...

    socket = new MulticastSocket();
    group  = InetAddress.getByName(MDNS_GROUP_ADDRESS);
    socket.joinGroup(group);
    
    ...

where MDNS_GROUP_ADDRESS is defined as

    private static final String MDNS_GROUP_ADDRESS = "224.0.0.251";

1.2 Sending

Sending a datagram is also the same as in the unicast case, e.g.,

    ...
    
    packet.setAddress(group);
    packet.setPort(MDNS_PORT);
    socket.send(packet);

    ...

where packet is an instance of java.net.DatagramPacket, group is the IV we initialized when creating the socket and MDNS_PORT isdefined as

    private static final int MDNS_PORT = 5353;

1.3 Receiving

Receiving on a multicast socket is exactly the same as on a unicast socket

2.0 Reading A DNS Message

The class MessageReader is responsible for reading incoming DNS messages. Only responses are handled as that is all we expect to see.

The components of the response are handed to an instance of ResponseHandler as they are read.

These are the methods that read the message.

    ...
    
    public void read(ResponseHandler theHandler)
                throws
                    Exception
    {
        int id        = readUnsignedShort();
        int bitfields = readUnsignedShort();
    
        if ((bitfields & QR_RESPONSE_BIT) == 0)
        {
            // query !
        }
        else
        if ((bitfields & RCODE_BITS) == 0)
        {
            theHandler.beginResponse();
    
            int qdcount = readUnsignedShort();
            int ancount = readUnsignedShort();
            int nscount = readUnsignedShort();
            int arcount = readUnsignedShort();
    
            readQuestions(qdcount, theHandler);
            readRecords(Category.ANSWER, ancount, theHandler);
            readRecords(Category.AUTHORITY, nscount, theHandler);
            readRecords(Category.ADDITIONAL, arcount, theHandler);
            if (offset != end)
            {
                throw new Exception();
            }
            theHandler.endResponse();
        }
        else
        {
            // error !
        }
    }
	
    private void readQuestions(int theCount, ResponseHandler theHandler)
                 throws
                     Exception
    {
        for (int i = 0; i < theCount; i++)
        {
            theHandler.question(
                           readNodeName(),
                           TYPE.get(readUnsignedShort()),
                           CLASS.get(readUnsignedShort()));
        }
    }
	
    private void readRecords(Category theCategory, int theCount, ResponseHandler theHandler)
                 throws
                     Exception
    {
        for (int i = 0; i < theCount; i++)
        {
            readRecord(theCategory, theHandler);
        }
    }
	
    private void readRecord(Category theCategory, ResponseHandler theHandler)
                 throws
                     Exception
    {
        String name     = readNodeName();
        int    type     = readUnsignedShort();
        int    klass    = readUnsignedShort();
        int    ttl      = readInt();
        int    rdlength = readUnsignedShort();
    
        switch (type)
        {
            case DNS.T_A:
    
                theHandler.a(theCategory, name, ttl, makeRDATA(rdlength));
                break;
    
            case DNS.T_AAAA:
    
                theHandler.aaaa(theCategory, name, ttl, makeRDATA(rdlength));
                break;
    
            case DNS.T_PTR:
    
                theHandler.ptr(theCategory, name, ttl, readNodeName());
                break;
    
            case DNS.T_TXT:
    
                theHandler.txt(theCategory, name, ttl, makeRDATA(rdlength));
                break;
    
            case DNS.T_SRV:
    
                theHandler.srv(
                               theCategory,
                               name,
                               ttl,
                               readUnsignedShort(),
                               readUnsignedShort(),
                               readUnsignedShort(),
                               readNodeName());
                break;
    
            default:
    
                theHandler.rr(theCategory, name, type, klass, ttl, makeRDATA(rdlength));
        }
    }
    
    ...

3.0 Writing a DNS Message

This is left as an exercise for the reader.

4.0 A Very Simple API

To do DNS base service discovery we need the ability to make queries and see the responses.

The class xper.net.mdns.Client defines the method

    public void query(String theName, TYPE theType)
                throws
                    Exception

which can be used to make queries.

An instance of the Client class can be obtained by calling the method

    public static Client getClient(ResponseHandler theHandler)

where the ResponseHandler interface is defined as follows

    public interface ResponseHandler
    {
        public enum Category
        {
            ANSWER,
            AUTHORITY,
            ADDITIONAL,
        }
	
        //
	
        public void beginResponse();
	
        //
	
        public void question(String theName, TYPE theType, CLASS theClass);
	
        //
	
        public void a(Category theCategory, String name, int ttl, RDATA makeRDATA);
	
        public void aaaa(Category theCategory, String name, int ttl, RDATA makeRDATA);
    
        public void ptr(Category theCategory, String theName, int theTTL, String thePtr);
	
        public void srv(
                        Category theCategory, 
                        String   theName, 
                        int      theTTL, 
                        int      thePriority, 
                        int      theWeight, 
                        int      thePort, 
                        String   theTarget);
	
        public void txt(Category theCategory, String theName, int theTTL, RDATA theData);
	
        //
	
        public void rr(Category theCategory, String theName, int theType, int theClass, int theTTL, RDATA theData);
    
        //
	
        public void endResponse();
    
    }

5.0 The FindServices Class

The FindServices class uses the ‘API’ to find services of a given type.

    // FindServices.java
    
    // Copyright (c) 2013 By Simon Lewis. All Rights Reserved.
    
    package xper.net.sd;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import android.util.Log;
    
    import xper.net.dns.core.CLASS;
    import xper.net.dns.core.RDATA;
    import xper.net.dns.core.ResponseHandler;
    import xper.net.dns.core.TYPE;
    import xper.net.mdns.Client;
    
    public final class FindServices
                       implements
                           ResponseHandler
    {
        public FindServices(String theType, ServiceListener theListener)
        {
            client   = Client.getClient(this);
            type     = theType;
            listener = theListener;
            services = new HashMap<String, ServiceData>();
        }
        
        //
        
        public void start()
                    throws
                        Exception
        {
            log("start");
            client.query(type, TYPE.PTR);
        }
        
        //
        
        @Override
        public void beginResponse()
        {
            log(">> beginResponse");
        }
        
        @Override
        public void question(String theName, TYPE theType, CLASS theClass)
        {
            log("question: " + theName + " " + theType + " " + theClass);
        }
        
        @Override
        public void a(Category theCategory, String theName, int theTTL, RDATA theData)
        {
            log(theCategory + " A: " + theName);
        }
        
        @Override
        public void aaaa(Category theCategory, String theName, int theTTL, RDATA theData)
        {
            log(theCategory + " AAAA: " + theName);
        }
        
        @Override
        public void ptr(Category theCategory, String theName, int theTTL, String thePtr)
        {
            log(theCategory + " PTR: " + thePtr);
		
            switch (theCategory)
            {
                case ANSWER:
        
                    ptr(theName, thePtr);
                    break;
        
                default:
        
                    ;
            }
        }
        
        @Override
        public void srv(
                        Category theCategory, 
                        String   theName, 
                        int      theTTL, 
                        int      thePriority, 
                        int      theWeight, 
                        int      thePort, 
                        String   theTarget)
        {
            log(theCategory + " SRV: " + theName);
		
            switch (theCategory)
            {
                case ANSWER:
                case ADDITIONAL:
        
                    srv(theName, thePriority, theWeight, thePort, theTarget);
                    break;
        
                default:
        
                    ;
            }
        }
        
        @Override
        public void txt(Category theCategory, String theName, int theTTL, RDATA theData)
        {
            log(theCategory + " TXT: " + theName);
		
            switch (theCategory)
            {
                case ANSWER:
                case ADDITIONAL:
        
                    txt(theName, theData);
                    break;
        
                default:
        
                    // Ignore
                    ;
            }
        }
        
        @Override
        public void rr(Category theCategory, String theName, int theType, int theClass, int theTTL, RDATA theData)
        {
            log(theCategory + " rr: " + theName + " type == " + theType + " class == " + theClass);
        }
        
        @Override
        public void endResponse()
        {
            for (ServiceData s: services.values())
            {
                try
                {
                    if (!s.haveSRV)
                    {
                        client.query(s.name, TYPE.SRV);
                    }
                    else
                    if (!s.haveTXT)
                    {
                        client.query(s.name, TYPE.TXT);
                    }
                    else
                    if (!s.found)
                    {
                        log("Found service " + s.name);
                        s.found = true;
                        listener.serviceFound(s);
                    }
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
            log("<< endResponse");
        }
            
        //
            
        private void ptr(String theName, String theServiceName)
        {
            ServiceData service    = getService(theServiceName);
            
            if (service == null)
            {
                addService(theServiceName);
            }
            else
            {
                // ????
            }
        }
            
        private void srv(String theName, int thePriority, int theWeight, int thePort, String theTarget)
        {
            ServiceData service = getService(theName);
            
            if ((service != null) && !service.haveSRV)
            {
                log("priority == " + thePriority);
                log("weight   == " + theWeight);
                log("port     == " + thePort);
                log("target   == " + theTarget);
			
                service.target   = theTarget;
                service.port     = thePort;
                service.priority = thePriority;
                service.weight   = theWeight;
                service.haveSRV  = true;
            }
            else
            {
                log("Unsolicited SRV record for " + theName + " !");
            }
        }
            
        private void txt(String theName, RDATA theData)
        {
            ServiceData service = getService(theName);
            
            if ((service != null) && !service.haveTXT)
            {
                try
                {
                    service.keyValuePairs = makeKeyValuePairs(theData.asStrings());
                    service.haveTXT       = true;
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
            else
            {
                log("Unsolicited TXT record for " + theName + " !");
            }
        }
            
        //
            
        private Map<String, String> makeKeyValuePairs(List theRawStrings)
        {
            Map<String, String> keyValuePairs = new HashMap<String, String>();
            
            for (String s : theRawStrings)
            {
                String key   = null;
                String value = null;
                int    index = s.indexOf('=');
			
                if (index != -1)
                {
                    key   = s.substring(0, index);
                    value = s.substring(index + 1);
                }
                else
                {
                    key   = s;
                    value = "";
                }
                log("key == " + key + " value == " + value);
                if (keyValuePairs.get(key) == null)
                {
                    keyValuePairs.put(key, value);
                }
            }
            return keyValuePairs;
        }
            
        //
            
        private ServiceData getService(String theName)
        {
            return services.get(theName);
        }
            
        private void addService(String theName)
        {
            services.put(theName, new ServiceData(theName, type));
        }
            
        private void log(String theString)
        {
            Log.d("FindServices", theString);
        }
            
        //
            
        private Client                   client;
        private String                   type;
        private ServiceListener          listener;
        private Map<String, ServiceData> services;
            
            
        private static final class ServiceData
                                   implements
                                       Service
        {
            @Override
            public String getName()
            {
                return name;
            }
            
            @Override
            public String getType()
            {
                return type;
            }
            
            @Override
            public String getTarget() 
            {
                return target;
            }
            
            @Override
            public int getPort() 
            {
                return port;
            }
            
            @Override
            public int getPriority() 
            {
                return priority;
            }
            
            @Override
            public int getWeight() 
            {
                return weight;
            }
            
            @Override
            public Map<String, String> getKeyValuePairs()
            {
                return keyValuePairs;
            }
            
            ServiceData(String theName, String theType) 
            {
                name = theName;
                type = theType;
            }
            
            //
            
            private String              name;
            private String              type;
            private String              target;
            private int                 port;
            private int                 priority;
            private int                 weight;
            private Map<String, String> keyValuePairs;
            
            //
            
            private boolean             haveSRV;
            private boolean             haveTXT;
            private boolean             found;
        }
    }

6.0 Examples

The following examples all show the output from an instance of the FindServices class initialized with the service type

    _ipp._tcp.local.

All output is the result of running the output of adb logcat through the command grep FindServices

All the examples involve combinations of three hosts on a wifi network.

6.1 Example One

An Android device and a actual printer.

It takes three queries to obtain all the necessary information


    D/FindServices( 2549): start
    D/FindServices( 2549): >> beginResponse
    D/FindServices( 2549): question: _ipp._tcp.local. PTR IN
    D/FindServices( 2549): ANSWER PTR: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): << endResponse
    D/FindServices( 2549): >> beginResponse
    D/FindServices( 2549): question: Canon MG6200 series._ipp._tcp.local. SRV IN
    D/FindServices( 2549): ANSWER SRV: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): priority == 0
    D/FindServices( 2549): weight   == 0
    D/FindServices( 2549): port     == 631
    D/FindServices( 2549): target   == 7D300C000000.local.
    D/FindServices( 2549): << endResponse
    D/FindServices( 2549): >> beginResponse
    D/FindServices( 2549): question: Canon MG6200 series._ipp._tcp.local. TXT IN
    D/FindServices( 2549): ANSWER TXT: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): key == txtvers value == 1
    D/FindServices( 2549): key == rp value == ipp/printer
    D/FindServices( 2549): key == note value ==
    D/FindServices( 2549): key == qtotal value == 1
    D/FindServices( 2549): key == priority value == 15
    D/FindServices( 2549): key == ty value == Canon MG6200 series
    D/FindServices( 2549): key == product value == (Canon MG6200 series)
    D/FindServices( 2549): key == pdl value == application/octet-stream,image/urf,image/jpeg
    D/FindServices( 2549): key == adminurl value == http://7D300C000000.local.
    D/FindServices( 2549): key == usb_MFG value == Canon
    D/FindServices( 2549): key == usb_MDL value == MG6200 series
    D/FindServices( 2549): key == usb_CMD value == URF
    D/FindServices( 2549): key == UUID value == 00000000-0000-1000-8000-8887177D300C
    D/FindServices( 2549): key == URF value == CP1,PQ4-5,RS600,SRGB24,W8,DM3,OB9,OFU0
    D/FindServices( 2549): key == Color value == T
    D/FindServices( 2549): key == Duplex value == T
    D/FindServices( 2549): key == Scan value == T
    D/FindServices( 2549): key == mac value == 88:87:17:7D:30:0C
    D/FindServices( 2549): Found service Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): << endResponse

6.2 Example Two

An Android device and a Mac.

The Mac is running an instance of the CUPS 1.7.0 test server ippserver with the name ipp_server_1 is which listening on port 6363.

The MacOS X mDNS implementation, mDNSResponder, is following the recommendation in RFC 6763 and is returning the relevant SRV and TXT records as well as the AAAA and A records as additional records in the response to the original query.


    D/FindServices( 2815): start
    D/FindServices( 2815): >> beginResponse
    D/FindServices( 2815): question: _ipp._tcp.local. PTR IN
    D/FindServices( 2815): ANSWER PTR: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): ADDITIONAL SRV: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): priority == 0
    D/FindServices( 2815): weight   == 0
    D/FindServices( 2815): port     == 6363
    D/FindServices( 2815): target   == Simons-Computer.local.
    D/FindServices( 2815): ADDITIONAL TXT: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): key == rp value == ipp/print
    D/FindServices( 2815): key == ty value == Test Printer
    D/FindServices( 2815): key == adminurl value == http://Simons-Computer.local:6363/
    D/FindServices( 2815): key == product value == (Printer)
    D/FindServices( 2815): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 2815): key == Color value == F
    D/FindServices( 2815): key == Duplex value == F
    D/FindServices( 2815): key == usb_MFG value == Test
    D/FindServices( 2815): key == usb_MDL value == Printer
    D/FindServices( 2815): ADDITIONAL AAAA: Simons-Computer.local.
    D/FindServices( 2815): ADDITIONAL A: Simons-Computer.local.
    D/FindServices( 2815): Found service ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): << endResponse

6.3 Example Three

An Android device and a Mac.

The Mac is running two instances of ippserver.

As in Example Two all the necessary information is contained in the reply to the original query.

        
    D/FindServices( 2929): start
    D/FindServices( 2929): >> beginResponse
    D/FindServices( 2929): question: _ipp._tcp.local. PTR IN
    D/FindServices( 2929): ANSWER PTR: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): ANSWER PTR: ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): ADDITIONAL SRV: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): priority == 0
    D/FindServices( 2929): weight   == 0
    D/FindServices( 2929): port     == 6363
    D/FindServices( 2929): target   == Simons-Computer.local.
    D/FindServices( 2929): ADDITIONAL TXT: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): key == rp value == ipp/print
    D/FindServices( 2929): key == ty value == Test Printer
    D/FindServices( 2929): key == adminurl value == http://Simons-Computer.local:6363/
    D/FindServices( 2929): key == product value == (Printer)
    D/FindServices( 2929): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 2929): key == Color value == F
    D/FindServices( 2929): key == Duplex value == F
    D/FindServices( 2929): key == usb_MFG value == Test
    D/FindServices( 2929): key == usb_MDL value == Printer
    D/FindServices( 2929): ADDITIONAL SRV: ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): priority == 0
    D/FindServices( 2929): weight   == 0
    D/FindServices( 2929): port     == 6364
    D/FindServices( 2929): target   == Simons-Computer.local.
    D/FindServices( 2929): ADDITIONAL TXT: ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): key == rp value == ipp/print
    D/FindServices( 2929): key == ty value == Test Printer
    D/FindServices( 2929): key == adminurl value == http://Simons-Computer.local:6364/
    D/FindServices( 2929): key == product value == (Printer)
    D/FindServices( 2929): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 2929): key == Color value == F
    D/FindServices( 2929): key == Duplex value == F
    D/FindServices( 2929): key == usb_MFG value == Test
    D/FindServices( 2929): key == usb_MDL value == Printer
    D/FindServices( 2929): ADDITIONAL AAAA: Simons-Computer.local.
    D/FindServices( 2929): ADDITIONAL A: Simons-Computer.local.
    D/FindServices( 2929): Found service ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): Found service ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): << endResponse
    

6.4 Example Four

An Android device, a printer and a Mac running an instance of ippserver.

Just to show it can be done.


    D/FindServices( 3453): start
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: _ipp._tcp.local. PTR IN
    D/FindServices( 3453): ANSWER PTR: ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): ADDITIONAL SRV: ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): priority == 0
    D/FindServices( 3453): weight   == 0
    D/FindServices( 3453): port     == 6363
    D/FindServices( 3453): target   == Simons-Computer.local.
    D/FindServices( 3453): ADDITIONAL TXT: ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): key == rp value == ipp/print
    D/FindServices( 3453): key == ty value == Test Printer
    D/FindServices( 3453): key == adminurl value == http://Simons-Computer.local:6363/
    D/FindServices( 3453): key == product value == (Printer)
    D/FindServices( 3453): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 3453): key == Color value == F
    D/FindServices( 3453): key == Duplex value == F
    D/FindServices( 3453): key == usb_MFG value == Test
    D/FindServices( 3453): key == usb_MDL value == Printer
    D/FindServices( 3453): ADDITIONAL AAAA: Simons-Computer.local.
    D/FindServices( 3453): ADDITIONAL A: Simons-Computer.local.
    D/FindServices( 3453): Found service ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): << endResponse
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: _ipp._tcp.local. PTR IN
    D/FindServices( 3453): ANSWER PTR: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): << endResponse
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: Canon MG6200 series._ipp._tcp.local. SRV IN
    D/FindServices( 3453): ANSWER SRV: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): priority == 0
    D/FindServices( 3453): weight   == 0
    D/FindServices( 3453): port     == 631
    D/FindServices( 3453): target   == 7D300C000000.local.
    D/FindServices( 3453): << endResponse
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: Canon MG6200 series._ipp._tcp.local. TXT IN
    D/FindServices( 3453): ANSWER TXT: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): key == txtvers value == 1
    D/FindServices( 3453): key == rp value == ipp/printer
    D/FindServices( 3453): key == note value ==
    D/FindServices( 3453): key == qtotal value == 1
    D/FindServices( 3453): key == priority value == 15
    D/FindServices( 3453): key == ty value == Canon MG6200 series
    D/FindServices( 3453): key == product value == (Canon MG6200 series)
    D/FindServices( 3453): key == pdl value == application/octet-stream,image/urf,image/jpeg
    D/FindServices( 3453): key == adminurl value == http://7D300C000000.local.
    D/FindServices( 3453): key == usb_MFG value == Canon
    D/FindServices( 3453): key == usb_MDL value == MG6200 series
    D/FindServices( 3453): key == usb_CMD value == URF
    D/FindServices( 3453): key == UUID value == 00000000-0000-1000-8000-8887177D300C
    D/FindServices( 3453): key == URF value == CP1,PQ4-5,RS600,SRGB24,W8,DM3,OB9,OFU0
    D/FindServices( 3453): key == Color value == T
    D/FindServices( 3453): key == Duplex value == T
    D/FindServices( 3453): key == Scan value == T
    D/FindServices( 3453): key == mac value == 88:87:17:7D:30:0C
    D/FindServices( 3453): Found service Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): << endResponse


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

Service Discovery In Android And iOS: Part Three – If You Want Something Done Properly … mDNS And DNS Service Discovery Basics

To continue with the wild over simplifications, Multicast DNS (mDNS) defines a special node with the label

    local

immediately beneath the root ot the DNS name space.

The DNS name space sub-tree beneath the local node is not global but confined to the hosts directly attached to a given network.

There is no dedicated DNS server responsible for managing a local sub-tree.

Instead individual hosts attached to a network can each allocate nodes within the local sub-tree themselves. This means that different hosts can allocate nodes with the same name with associated resource records of the same type and class.

For queries to work in this context they are sent via multicast UDP so that they can be seen by all the hosts in the local network which may have allocated nodes in the sub-tree. Queries may result in multiple responses each from a different host.

2.0 Service Discovery Using DNS

RFC 6763 DNS-Based Service Discovery specifies how the DNS name space and DNS resource records can be used to make services discoverable.

2.1 Service Types

The label of a node which represents a service type starts with an underscore (‘_’).

A node which represents a service type is either a child of a node with the label

    _tcp

meaning that the service is available via TCP, or a child of a node with the label

    _udp

which, curiously, does not necessarily mean that the service is available via UDP, although it can be, but that the service is available via some protocol other than TCP.

For example the node

    _ipp._tcp.local.

represents the IPP over TCP service type in the local domain

2.2 Service Instances

A service instance, i.e., an entity which implements a service of a given type, is represented by child node of the service type node, e.g.,

    ipp-printer._ipp._tcp.local.

2.3 Service Type Resource Records

A service type node has an associated PTR record for each service instance of that service type. The data in each PTR record is the name of the node representing the service instance.

2.4 Service Instance Records

A service instance node has an associated SRV record.

The SRV record data identifies the host on which the service instance is running and the port it is listening on.

A service instance node also has an associated TXT record.

2.4.1 Service Instance TXT Records

The data of a service instance TXT record comprises one or more of strings of the form

    key=value

The key value pairs provide additional information about the service.

2.5 Service Discovery

To discover all the instances of a given service type it is only necessary to query the appropriate service type node for its PTR records.

Each PTR record in the answer will identify a service instance.

The node for each service instance can then be queried for its SRV and TXT records.

2.6 Service Discovery And The Additional Records Section

The standard recommends that the associated SRV and TXT records of the service instancea be included in the additional records section of the response to the query to the service type node for its PTR records.

In theory therefore it is possible to obtain all the information about the available instances of a given service type with a single query, subject to limits on the datagram size.


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

October 31, 2013

Service Discovery In Android And iOS: Part Two – If You Want Something Done Properly … DNS Basics

1.0 Overview

Over simplifying wildly, the Domain Name System (DNS) defines a global tree-structured name space.

Each interior node and leaf node in the tree identifies a resource and can have zero or more associated resource records.

Each node in the tree has a label.

The name of a node is written as a dot separated list of the series of labels that result from traversing from that node to the root of the tree, with the root of the tree on the right

The label of the root node is the empty string so all node names end with a dot (.).

Each resource record has a class and a type.

A given DNS server maintains a database of all the resource records associated with a particular sub-tree of the entire name space.

Clients of the server can issue queries against the server’s database.

A query to a DNS server is a request for a set of resource records associated with a given node.

The query specifies a node within the DNS name space and the required record class and type.

If the specified node has a associated records of the given class and type they are returned in the response to the query.

In certain circumstances a DNS server may obtain an answer to a query it receives by performing a recursive query, that is, by itself querying another DNS name server.

Queries can be sent using either UDP or TCP. In both cases a DNS server listens on port 53.

2.0 Record Types

There are a large number of defined DNS record types.

For the purposes of service discovery we are only interested in three types

  • PTR
  • SRV
  • TXT

but we may see two further types

  • A
  • AAAA

2.1 The A Record

An A record contains an IPv4 address.

2.2 The AAAA Record

An AAAA record contains an IPv6 address.

2.3 The PTR Record

A PTR record contains the name of a node in the DNS name space.

2.4 The SRV Record

A SRV record contains information about a server.

2.5 The TXT Record

A TXT record contains arbitrary text.

3.0 DNS Messages

Queries to a DNS server and responses from it use a single message format.

3.1 The DNS Message Format

dns_message

3.1.1 Questions

A sequence of zero or more Questions.

If the message is a query than this section contains the question expressing the query.

If the message is a response than this section contains the question sent in the query to which this is the response.

3.1.2 Answers

A sequence of zero or more resource records.

If the message is a non-error response then this section contains the resource record(s) which match the query to which this is the response.

3.1.3 Authority

A sequence of zero or more resource records.

If the message is an error response then this section may contain resource record(s) identifying DNS servers which can be queried instead.

3.1.4 Additional

A sequence of zero or more Resource records.

If the message is a non-error response then this section may contain resource records, which do not match the query, but are related to it, e.g., other resource records for the same node.

3.2 The Header Format

header

3.2.1 The Message Id

If the message is a query then this is the id of the query allocated by the entity performing the query.

If the message is a response then this is the id of the query to which this is the response.

3.2.2 QR

A single bit field

0 if the message contains a query.

1 if the message contains a response.

3.2.3 Op Code

A four bit field

0 for a standard query

3.2.4 AA

A single bit field

Only valid in a response.

If set then the DNS server is the authority for the node specified in the question

3.2.5 TC

A single bit field

If set then this message was truncated.

3.2.6 RD

A single bit field

If set in a query then the server is requested to perform a recursive query if necessary.

If set in the query it will also bet set in the response.

3.2.7 RA

A single bit field

Only valid in a response.

If set then the server is capable of performing recursive queries.

3.2.7 Z

A three bit field

Not used. Reserved.

3.2.8 Response Code

A four bit field.

Only valid in a response.

0 if no error occurred.

3.2.9 Question Count

A 16-bit unsigned integer.

The number of questions in the questions section of this message.

3.2.10 Answer Count

A 16-bit unsigned integer.

The number of resource records in the answers section of this message.

3.2.11 Authority Count

A 16-bit unsigned integer.

The number of resource records in the authority section of this message.

3.2.12 Additional Count

A 16-bit unsigned integer.

The number of resource records in the additional section of this message.

3.3 The Question Format

question

3.3.1 Name

An N byte field.

The name of the node whose resource records are being requested.

3.3.2 Type

A 16-bit field.

The type of the requested resource record(s).

3.3.3 Class

A 16-bit field.

The class of the requested resource record(s).

3.4 The Resource Record Format

rr

3.4.1 Name

An N byte field.

The encoded name of the node to which this resource record applies.

3.4.2 Type

A 16-bit field.

The type of this resource record.

3.4.3 Class

A 16-bit field.

The class of this resource record.

3.4.4 TTL

A 32-bit unsigned integer.

The number of seconds after which this record is no longer valid.

3.4.5 Record Data Length

A 16-bit unsigned integer field.

The length in bytes of the data that follows.

3.4.6 Record Data

An N byte field.

Contains the type specific record data. (see below)

3.5 Classes

Although there are a number of DNS classes defined the class of a resource record is usually IN (Internet) which has the value 1.

3.6 Types

Resource record types are specified using 16-bit integers.

For the resource record types we are interested or which we might see the values are as follows

Record Type Value
A 1
AAAA 28
PTR 12
SRV 33
TXT 16

3.7 Node Names

3.7.1 Basic Encoding

In the basic encosing a node name is encoded as a sequence of labels.

Each label is encoded as a byte field specifying the length of the label followed by the bytes encoding the label.

Since the root label is the empty string an node name encoded in this way is always terminated by a zero byte.

3.7.1 Compressed Encoding

Alternatively all of a node name or the suffix of a node name can be encoded as a reference to all or the suffix of a previous node name in the message.

If the top two bits of the byte encoding the label length are set then the other 6-bits of the byte and the 8-bits of following byte specify the offset in the message of the ‘rest’ of the node name.

4.0 Specific Record Data Formats

4.1 A Record Data Format

A 32-bit IPv4 address in network byte order.

4.2 AAAA Record Data Format

A 128-bit IPV6 address in network byte order.

4.1 PTR Record Data Format

An encoded node name.

4.3 SRV Record Data Format

srv_data_format

4.3.1 Priority

A 16-bit unsigned int in network byte order.

Intended to be used by a client to choose between servers.

The client should attempt to use the server with the lowest priority.

4.3.2 Weight

A 16-bit unsigned int network byte order.

Intended to be used by a client to choose between servers of the same priority.

The client should attempt to use the server with the highest weight.

4.3.2 Port

A 16-bit unsigned int in network byte order.

The port on which the server is listening.

4.3.3 Target

An N byte field.

The encoded name of the node which represents the host on which the server is running.

4.4 TXT Record Data Format

The format of the data of a TXT record is context specific.


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

October 29, 2013

Service Discovery in Android And iOS – Part One: The android.net.nsd.NsdManager Class Considered Not Very Useful At All

Suppose I was writing an Android program and for some strange reason I wanted to automatically find any printers on the local network that support the Internet Printing Protocol (IPP).

Quite why I would want to do this is anyone’s guess. Perhaps I want to do something bizarre like support printing from
my program.

Anyway, in theory I am in luck because I can use an instance of the android.net.NsdManager class which supports
mDNS based service discovery.

1.0 Getting An NsdManager Instance

An instance of android.net.nsd.NsdManager can be obtained by a call to the Context.getSystemService
method with the argumentcContext.NSD_SERVICE.

So in an Application’s main Activity we can do something like this

    NsdManager manager = (NsdManager)getSystemService(NSD_SERVICE);

2.0 Starting Service Discovery

The process of service discovery is started by invoking the NsdManager.discoverServices method which is defined like this

    public void discoverServices(String serviceType, int protocolType, NsdManager.DiscoveryListener listener)

The methods defined by the DiscoveryListener instance passed as the listener argument are invoked asynchronously.

3.0 android.net.nsd.NsdManager.DiscoveryListener

The DiscoveryListener interface defines two methods to report the outcome of starting the process of service discovery

    public void onDiscoveryStarted(String serviceType)

    public void onStartDiscoveryFailed(String serviceType, int errorCode)

two methods to report the outcome of stopping the process of service discovery

    public void onDiscoveryStopped(String serviceType)

    public void onStopDiscoveryFailed(String serviceType, int errorCode)

a method to report that a service has been discovered

    public void onServiceFound(NsdServiceInfo serviceInfo)

and a method to report that a previously discovered service has disappeared.

    public void onServiceLost (NsdServiceInfo serviceInfo)

4.0 Resolving A Service

If the onServiceFound method of the DiscoveryListener instance passed to the call to discoverServices is invoked then a service has been discovered.

We can then call the NsdManager.resolveService which is declared like this

    public void resolveService(NsdServiceInfo serviceInfo, NsdManager.ResolveListener listener)

The serviceInfo argument is NsdServiceInfo instance passed to onServiceFound.

The methods defined by the ResolveListener instance passed as the listener argument are invoked asynchronously.

5.0 android.net.nsd.NsdManager.ResolveListener

The ResolveListener interface declares a method to report that a service has been resolved

    public void onServiceResolved(NsdServiceInfo serviceInfo)

and a method to report that it was not possible to resolve a service

    public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode)

6.0 The FindServicesNSD Class

Here is the source code for the FindServicesNSD class which encapsulates the use of the NsdManager service
discovery methods

    // FindServicesNSD.java
    
    // Copyright (c) 2013 By Simon Lewis. All Rights Reserved.
    
    package xper.villiars;
    
    import android.net.nsd.NsdManager;
    import android.net.nsd.NsdManager.DiscoveryListener;
    import android.net.nsd.NsdManager.ResolveListener;
    import android.net.nsd.NsdServiceInfo;
    import android.util.Log;
    
    final class FindServicesNSD
                implements
                    DiscoveryListener,
                    ResolveListener
    {
        // DiscoveryListener
	
        @Override
        public void onDiscoveryStarted(String theServiceType)
        {
            Log.d(TAG, "onDiscoveryStarted");
        }
    
        @Override
        public void onStartDiscoveryFailed(String theServiceType, int theErrorCode)
        {
            Log.d(TAG, "onStartDiscoveryFailed(" + theServiceType + ", " + theErrorCode);
        }
	
        @Override
        public void onDiscoveryStopped(String serviceType)
        {
            Log.d(TAG, "onDiscoveryStopped");
        }
	
        @Override
        public void onStopDiscoveryFailed(String theServiceType, int theErrorCode)
        {
            Log.d(TAG, "onStartDiscoveryFailed(" + theServiceType + ", " + theErrorCode);
        }
	
        @Override
        public void onServiceFound(NsdServiceInfo theServiceInfo)
        {
            Log.d(TAG, "onServiceFound(" + theServiceInfo + ")");
            Log.d(TAG, "name == " + theServiceInfo.getServiceName());
            Log.d(TAG, "type == " + theServiceInfo.getServiceType());
            serviceFound(theServiceInfo);
        }
    
        @Override
        public void onServiceLost(NsdServiceInfo theServiceInfo)
        {
            Log.d(TAG, "onServiceLost(" + theServiceInfo + ")");
        }
    
        // Resolve Listener
    
        @Override
        public void onServiceResolved(NsdServiceInfo theServiceInfo)
        {
            Log.d(TAG, "onServiceResolved(" + theServiceInfo + ")");
            Log.d(TAG, "name == " + theServiceInfo.getServiceName());
            Log.d(TAG, "type == " + theServiceInfo.getServiceType());
            Log.d(TAG, "host == " + theServiceInfo.getHost());
            Log.d(TAG, "port == " + theServiceInfo.getPort());
	    }
	
        @Override
        public void onResolveFailed(NsdServiceInfo theServiceInfo, int theErrorCode)
        {
            Log.d(TAG, "onResolveFailed(" + theServiceInfo + ", " + theErrorCode);
        }
    
        //
	
        FindServicesNSD(NsdManager theManager, String theServiceType)
        {
            manager     = theManager;
            serviceType = theServiceType;
        }
	
        void run()
        {
            manager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, this);
        }
	
        private void serviceFound(NsdServiceInfo theServiceInfo)
        {
            manager.resolveService(theServiceInfo, this);
        }
	
        //
	
        private NsdManager   manager;
        private String       serviceType;
	
        //
	
        private static final String TAG = "FindServicesNSD";
    }

7.0 Example

Running an instance of FindServicesNSD created like this

    new FindServicesNSD((NsdManager)getSystemService(NSD_SERVICE), "_ipp._tcp")

on a device on a network with an IPP capable printer present results in the following


    D/FindServicesNSD( 2459): onDiscoveryStarted
    D/FindServicesNSD( 2459): onServiceFound(name:\
        Canon MG6200 seriestype: _ipp._tcp.host: nullport: 0txtRecord: null)
    D/FindServicesNSD( 2459): name == Canon MG6200 series
    D/FindServicesNSD( 2459): type == _ipp._tcp.
    D/FindServicesNSD( 2459): onServiceResolved(name:\
        Canon32MG620032seriestype: ._ipp._tcphost: /10.0.1.5port: 631txtRecord: null)
    D/FindServicesNSD( 2459): name == Canon32MG620032series
    D/FindServicesNSD( 2459): type == ._ipp._tcp
    D/FindServicesNSD( 2459): host == /10.0.1.5
    D/FindServicesNSD( 2459): port == 631

This is output from adb logcat run through grep FindSevicesNSD then slightly edited for clarity.

8.0 android.net.nsd.NsdServiceInfo: Where’s The Rest Of It ?

Given the instance of NsdServiceInfo passed to the onServiceResolved method it is possible to determine, the name and type of the service, the host on which it is running and the port it is listening on.

There is however additional per service information available via the mDNS service discovery mechanism which is not made accessible.[1]

While it may be possible to use a service without the missing information, this is not guaranteed to be the case

Using a service supporting IPP is one example where at least one piece of the missing per-service information is necessary.

9.0 Conclusion

Although the NsdManager class is documented as supporting mDNS based service discovery the deficiency in the implementation means that it ends up being not very useful at all.

Notes

  1. They could have made it accessible, its just that they had it and then they threw it away !


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

July 28, 2013

The Great Android Security Hole Of ’08 ? – Appendix: JAR Signing The Easy Way (TM)

1.0 How To Sign A JAR The Easy Way (TM)

  1. You create a digital signature for the entire JAR file as specified by one of the more reputable digital signature standards, PKCS#1 for example.

  2. That’s it.

2.0 How To Verify A JAR That Has Been Signed The Easy Way (TM)

  1. You compute the digest of the entire JAR file and verify it using the digital signature.

  2. That’s it.

3.0 Packaging A JAR That Has Been Signed The Easy Way (TM)

Given a JAR and its signature and the certificate chain of the signer you can either package them together or separately.

3.1 Packaging Them Together

To package them together you can either invent your own mechanism, not recommended, or you can use an existing one such as CMS/PKCS#7 with the signed data i.e. the JAR file, inline.

3.2 Packaging Them Separately

3.2.1 Use CMS/PKCS#7

You can also use CMS/PKCS#7 to package the signature separately from the JAR file, i.e. with the signed data, the JAR file, out-of-line.

If you are going to all the trouble of using CMS/PKCS#7 its unclear why you would want to do this, but you can if you want.

3.2.2 Define A New File Format

The alternative is to define a new file format.

At a minimum the file needs to include the encrypted signature and the certificate chain for the signer but it could also include other information about the Application, like how big the JAR is and where to download it from and what permissions it needs and things like that.

Including additional information could potentially enable optimizations such as deciding that you do not trust the signer before you have even downloaded the JAR.

You could even come up with a fancy name for your new file format. Something like JAR Descriptor, or even Java Application Descriptor, or JAD for short.

4.0 The Advantages Of JAR Signing The Easy Way (TM)

The overwhelming advantage of JAR signing the Easy Way (TM) is simplicity, particularly the simplicity of verification.

The opportunity for getting things wrong is orders of magnitude less than when verifying a signed JAR and at the end of it you also know that what you have got is exactly what was signed.


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content

Older Posts »

The WordPress Classic Theme Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: