Confused about DNSServiceGetAddrInfo

I expect there is a shockingly obvious answer to this, but I've been stuck a couple of days on Problem Obvious and could use a tip / cake-with-file to escape from development jail.

I have used DNSServiceRef / Bonjour to advertise my service, and have used the same to get a list of what is advertised (using the hit on lo0 for the moment since still testing). So, now I have a machine name "mymachine.local." and the right port number to connect to. Yay!

What I can not figure out is how to get that information into a (IPV6) sockaddr so I can use it with connect. The point of confusion for me is that DNSServiceGetAddrInfo() does not take a port argument, and I see no other place to squeeze this information into the sockaddr returned by the DNSServiceGetAddrInfoReply.

If I just use a IPV6 socket with that sockaddr, I get back EADDRNOTAVAIL. Haven't tried IPv4. No error is returned from DNSServiceGetAddrInfo. I'm reading around that this may be because the port is 0, and indeed I can't find any spot in this pathway to squeeze that information in.

I'll attach an obligatory bit of code so that the reader may feel more grounded:

// My DNSServiceGetAddrInfoReply
void ServiceList::Node::AddressInfoCallback( DNSServiceRef __nonnull _sdRef,
                                             DNSServiceFlags _flags,
                                             uint32_t _interfaceIndex,
                                             DNSServiceErrorType _errorCode,
                                             const char * __nullable _hostname,
                                             const struct sockaddr * __nullable _address,
                                             uint32_t UNUSED _ttl, void * __nonnull context)
{
    printf( "AddressInfo: \"%s\"\n", _hostname);
    AddrInfo * info = (AddrInfo*) context;


    if( kDNSServiceErr_NoError != _errorCode || NULL == _hostname || NULL == _address)
    {
        LOG_ERROR("Failed to get address info on \"%s\"\n", (const char*) info->hostTarget);
        delete info;
        return;
    }

    int err = connect(info->socket, _address, _address->sa_len);  // returns EADDRNOTAVAIL on IPv6 socket. 

What am I really trying to do? I'd like to connect to the address and port that I from my DNSServiceResolveReply.

Accepted Reply

The Problem Obvious answer was that when connecting, you are supposed to fill out the sockaddr structure yourself. There is apparently no API for this, and my attempts to find one above just sent me down a rabbit hole that returned a const sockaddr structure without a port field. This is not the only variant of the sockaddr structure, however. There are also sockaddr_in and sockaddr_in6 which are both more suitable for this use. I was able to make my own struct, copy in the bits the OS provided then set the port and move on. I'm sure it is entirely understood by the experts in the art just how crufty this is by current API standards so I won't bedevil the point but at least in my newbie perspective this and the ridiculous endianness patch for the port was on the level of "Don't drink the water" and "Don't feed the grizzly bears" -- the sort of cultural information that your language professor reveals with evident glee because it justifies perpetual employment in any economy, that would otherwise be a unwelcome trap for the strangers in a strange land, to the level of "Why would I even want to visit?" had they known. I'm not sure this has a place in well designed API, but it is culture at this point, I am sure, and obviously not under Apple's control.

In any event, this seems to work for me, should any other woe begotten travelers find themselves detoured on this path.

// My DNSServiceGetAddrInfoReply
void ServiceList::Node::AddressInfoCallback( DNSServiceRef __nonnull _sdRef,
                                             DNSServiceFlags _flags,
                                             uint32_t _interfaceIndex,
                                             DNSServiceErrorType _errorCode,
                                             const char * __nullable _hostname,
                                             const struct sockaddr * __nullable _address,
                                             uint32_t UNUSED _ttl, void * __nonnull context)
{
    AddrInfo * info = (AddrInfo*) context;


    if( kDNSServiceErr_NoError != _errorCode || NULL == _hostname || NULL == _address)
    {
        LOG_ERROR("Failed to get address info on \"%s\"\n", (const char*) info->hostTarget);
        delete info;
        return;
    }
    
    // set the port
    union {
        sockaddr_in    in;
        sockaddr_in6   in6;
    } addrs;  memset( &addrs, 0, sizeof(addrs));
    socklen_t addrsSize = 0;
    switch( _address->sa_len)
    {
        case sizeof( sockaddr_in):
            addrsSize = sizeof(addrs.in);
            memcpy( &addrs.in, _address, addrsSize);
            addrs.in.sin_port = htons(info->port);     // big endian. Because, obviously...
            break;
        case sizeof( sockaddr_in6):
            addrsSize = sizeof(addrs.in6);
            memcpy( &addrs.in6, _address, addrsSize);
            addrs.in6.sin6_port = htons(info->port);
            break;
        default:
            LOG_ERROR("Unhandled sockaddr variant");
            return;
    }
    int err = connect(info->socket, (sockaddr*) &addrs, addrsSize);
    ....

Replies

The Problem Obvious answer was that when connecting, you are supposed to fill out the sockaddr structure yourself. There is apparently no API for this, and my attempts to find one above just sent me down a rabbit hole that returned a const sockaddr structure without a port field. This is not the only variant of the sockaddr structure, however. There are also sockaddr_in and sockaddr_in6 which are both more suitable for this use. I was able to make my own struct, copy in the bits the OS provided then set the port and move on. I'm sure it is entirely understood by the experts in the art just how crufty this is by current API standards so I won't bedevil the point but at least in my newbie perspective this and the ridiculous endianness patch for the port was on the level of "Don't drink the water" and "Don't feed the grizzly bears" -- the sort of cultural information that your language professor reveals with evident glee because it justifies perpetual employment in any economy, that would otherwise be a unwelcome trap for the strangers in a strange land, to the level of "Why would I even want to visit?" had they known. I'm not sure this has a place in well designed API, but it is culture at this point, I am sure, and obviously not under Apple's control.

In any event, this seems to work for me, should any other woe begotten travelers find themselves detoured on this path.

// My DNSServiceGetAddrInfoReply
void ServiceList::Node::AddressInfoCallback( DNSServiceRef __nonnull _sdRef,
                                             DNSServiceFlags _flags,
                                             uint32_t _interfaceIndex,
                                             DNSServiceErrorType _errorCode,
                                             const char * __nullable _hostname,
                                             const struct sockaddr * __nullable _address,
                                             uint32_t UNUSED _ttl, void * __nonnull context)
{
    AddrInfo * info = (AddrInfo*) context;


    if( kDNSServiceErr_NoError != _errorCode || NULL == _hostname || NULL == _address)
    {
        LOG_ERROR("Failed to get address info on \"%s\"\n", (const char*) info->hostTarget);
        delete info;
        return;
    }
    
    // set the port
    union {
        sockaddr_in    in;
        sockaddr_in6   in6;
    } addrs;  memset( &addrs, 0, sizeof(addrs));
    socklen_t addrsSize = 0;
    switch( _address->sa_len)
    {
        case sizeof( sockaddr_in):
            addrsSize = sizeof(addrs.in);
            memcpy( &addrs.in, _address, addrsSize);
            addrs.in.sin_port = htons(info->port);     // big endian. Because, obviously...
            break;
        case sizeof( sockaddr_in6):
            addrsSize = sizeof(addrs.in6);
            memcpy( &addrs.in6, _address, addrsSize);
            addrs.in6.sin6_port = htons(info->port);
            break;
        default:
            LOG_ERROR("Unhandled sockaddr variant");
            return;
    }
    int err = connect(info->socket, (sockaddr*) &addrs, addrsSize);
    ....

This stuff is much easier if you use the Network framework rather than messing around with BSD Sockets.

If you are going to continue down the sockets path, I recommend that you read the BSD Sockets best practices section of TN3151 Choosing the right networking API. It lists many of the pitfalls you are likely to encounter.

Most notably, in the general case it’s not OK to resolve a service and then connect to the first address you get back. You need to implement Happy Eyeballs.

using the hit on lo0 for the moment since still testing

I’m not sure how to parse the above, so two comments:

  • If your final goal is to use Bonjour for discovery on the local machine, there’s a handy-handy DNS-SD option for that, namely kDNSServiceInterfaceIndexLocalOnly.

  • If you’re eventually going to reach out to off-device services, Happy Eyeballs starts to become really important.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"