getaddrinfo behaviour on 10.0.2

Hi,


With iOS 10.0.2, getaddrinfo (using AF_INET) is returning underlying IPv4 addresses of NAT64 IPv6 network (sahred from OS X network sharing) instead of failing. It is not the case for 10.0 or earlier versions where they provide error code on return (EAI_NONAME). We are also specificying AI_ADDRCONFIG flag, based on getaddrinfo man page, it should supress IPv4 addresses on NAT64 interfaces.

Is this intentional chage or bug ?

Replies

I’ve seen other reports of this (r. 28378416). However, please file your own bug about this because the other bug is kinda stuck waiting for more info from the developer. When you do file your bug, please include:

  • a sysdiagnose from the device (per this page)

  • information about which network the device is connected to

  • if you’re using VPN, information about that VPN

  • the output of the following commands run on a Mac connected to the same network as the device having problems

$ scutil --nwi
…
$ scutil --dns
…
$ dns-sd -G v4v6 ipv4only.arpa
…

Having said that, it’s best not to rely on the

AI_ADDRCONFIG
flag in the first place. Well-written BSD Sockets code does not need this flag, as I’ll explain below, and using it just serves to mask problems that you might otherwise find during testing.
getaddrinfo
returns a list of addresses, which may contain 0 or more IPv4 and 0 or more IPv6 addresses. Trying to determine, based solely on client state, which of these is the correct address is very hard to do. At a minimum, clients should attempt to connect to each address returned by
getaddrinfo
in the order in which they were returned;
getaddrinfo
works hard to return those addresses in the best possible order (per RFC 6724).

To improve on this, you can implement fast fallback (“happy eyeballs”) per RFC 6555.

To improve on this further, you can use a connect-by-name API. These implement an algorithm that uses platform-specific heuristics to improve on happy eyeballs.

IMPORTANT Using a connect-by-name API yields other benefits, most notably, compatibility with VPN On Demand.

Unfortunately there is no connect-by-name API at the BSD Sockets level (and I presume you’re using BSD Sockets otherwise why would you be using

getaddrinfo
in the first place?). One workaround that I’ve used for this in the past is to use a higher-level API (CFStream) to do the connect by name and then extract the connected socket from that API. Let me know if you’re interested in exploring this option.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks Quinn,


Here is the radar no. 28657896, it was created Yesterday., I will update the information next week.


Yes. I am using bsd sockets but It would be great if you share CFStream api with connect by name. I would like to give it a try with our cross platform library.

Here is the radar no. 28657896 …

Thanks!

Yes. I am using bsd sockets but It would be great if you share CFStream api with connect by name.

The specific API is

+[NSStream getStreamsToHostWithName:port:inputStream:outputStream:
] or, equivalently,
CFStreamCreatePairWithSocketToHost
. Once it’s connected, you can extract the socket using
kCFStreamPropertySocketNativeHandle
.

I’ve pasted in an example of this below; it’s kinda long, but there’s a lot of comments (-:

I would like to give it a try with our cross platform library.

Cool. The trick here is to cut the ‘head’ of your BSD Sockets code — that is, all the stuff that does the DNS resolution and then creates and connects the socket — while leaving behind the ‘tail’ to do all the I/O.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
/*! A structure holding various bits of context for the run loop callbacks.
*/

struct SuperConnectContext {
    uint32_t        magic;          ///< Must be kSuperConnectContextMagic.
    int            openCount;      ///< The number of times that the kCFStreamEventOpenCompleted event has been received.
    CFErrorRef      error;          ///< The first error noticed by the connection; must be released.
};
typedef struct SuperConnectContext SuperConnectContext;

enum {
    kSuperConnectContextMagic = 0xfaffd00d
};

/*! A private run loop mode used by all the run loop callbacks.
*/

static const CFStringRef kSuperConnectRunLoopMode = CFSTR("com.example.apple-samplecode.SuperConnect");

/*! Common code used by the read and write stream run loop callbacks.
*  \param type The type of stream event received.
*  \param info The stream callback's info pointer, which is really a pointer to a SuperConnectContext structure.
*  \param copyStreamErrorBlock A block that's called to extract the error from the stream when the event
*  is kCFStreamEventErrorOccurred.
*/

static void StreamCallbackCommon(CFStreamEventType type, void * info, CFErrorRef (^copyStreamErrorBlock)()) {
    SuperConnectContext *  context;

    context = (SuperConnectContext *) info;
    assert(context->magic == kSuperConnectContextMagic);

    switch (type) {
        case kCFStreamEventOpenCompleted: {
            context->openCount += 1;
        } break;
        case kCFStreamEventEndEncountered: {
            if (context->error == NULL) {
                context->error = CFErrorCreate(NULL, kCFErrorDomainPOSIX, EPIPE, NULL);
            }
        } break;
        case kCFStreamEventErrorOccurred:{
            if (context->error == NULL) {
                context->error = copyStreamErrorBlock();
            }
        } break;
    }
}

/*! A read stream callback.  This just forwards the event to StreamCallbackCommon.
*  \param stream The stream that received the event.
*  \param type The type of stream event.
*  \param info The info pointer for this callabck.
*/

static void ReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void * info) {
    StreamCallbackCommon(type, info, ^{
        return CFReadStreamCopyError(stream);
    });
}

/*! A write stream callback.  This just forwards the event to StreamCallbackCommon.
*  \param stream The stream that received the event.
*  \param type The type of stream event.
*  \param info The info pointer for this callabck.
*/

static void WriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void * info) {
    StreamCallbackCommon(type, info, ^{
        return CFWriteStreamCopyError(stream);
    });
}

/*! The run loop callback for the deadline timer.  This simply records the error in the context.
*  \param timer The timer that fired.
*  \param info The timer's info pointer, which is really a pointer to a SuperConnectContext structure.
*/

static void DeadlineTimerCallBack(CFRunLoopTimerRef timer, void *info) {
    #pragma unused(timer)
    SuperConnectContext *  context;

    context = (SuperConnectContext *) info;
    assert(context->magic == kSuperConnectContextMagic);

    // Record the error.

    if (context->error == NULL) {
        context->error = CFErrorCreate(NULL, kCFErrorDomainPOSIX, ETIMEDOUT, NULL);
    }

    // This is required to get the CFRunLoopRunInMode call in RunRunLoopWithDeadline to return
    // so that it can notice that context->error is set.

    CFRunLoopStop(CFRunLoopGetCurrent());
}

/*! Creates and configures streams that will connect to the specified port on the specified host.
*
*  IMPORTANT: This routine returns streams even when it fails.
*  \param context A pointer to the context used by the run loop callbacks.
*  \param hostName The host to connect to.
*  \param port The port on that host.
*  \param readStreamPtr A pointer to a stream; the pointer itself must not be NULL;
*  on entry, the pointed-to value is ignored; on return, it's set to the newly-created
*  stream.
*  \param writeStreamPtr A pointer to a stream; the pointer itself must not be NULL;
*  on entry, the pointed-to value is ignored; on return, it's set to the newly-created
*  stream.
*  \returns On success, 0; on error, an errno-style error number.
*/

static errno_t CreateAndOpenStreams(SuperConnectContext * context, CFStringRef hostName, int port, CFReadStreamRef * readStreamPtr, CFWriteStreamRef * writeStreamPtr) {
    Boolean                success;
    CFReadStreamRef        readStream;
    CFWriteStreamRef        writeStream;
    CFStreamClientContext  streamContext = { 0, context, NULL, NULL, NULL };

    CFStreamCreatePairWithSocketToHost(
        NULL,
        hostName,
        (UInt32) port,
        &readStream,
        &writeStream
    );

    success = CFReadStreamSetClient(  readStream, kCFStreamEventOpenCompleted | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered,  ReadStreamCallback, &streamContext);
    assert(success);
    success = CFWriteStreamSetClient(writeStream, kCFStreamEventOpenCompleted | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered, WriteStreamCallback, &streamContext);
    assert(success);

    CFReadStreamScheduleWithRunLoop(  readStream, CFRunLoopGetCurrent(), kSuperConnectRunLoopMode);
    CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(), kSuperConnectRunLoopMode);

    success = CFReadStreamOpen(readStream);
    if (success) {
        success = CFWriteStreamOpen(writeStream);
    }
    *readStreamPtr = readStream;
    *writeStreamPtr = writeStream;
    return success ? 0 : EINVAL;
}

/*! Runs the run loop until the connection completes, fails or times out.
*  \param context A pointer to the context used by the run loop callbacks.
*  \param deadline The timeout deadline.
*/

static void RunRunLoopWithDeadline(SuperConnectContext * context, CFAbsoluteTime deadline) {
    CFRunLoopTimerContext  timerContext  = { 0, context, NULL, NULL, NULL };
    CFRunLoopTimerRef      timer;

    timer = CFRunLoopTimerCreate(NULL, deadline, 0.0, 0, 0, DeadlineTimerCallBack, &timerContext);
    assert(timer != NULL);

    CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kSuperConnectRunLoopMode);

    do {
        (void) CFRunLoopRunInMode(kSuperConnectRunLoopMode, DBL_MAX, true);
        if ( (context->openCount == 2) || (context->error != NULL) ) {
            break;
        }
    } while (true);

    CFRunLoopTimerInvalidate(timer);
    CFRelease(timer);
}

/*! Extracts and returns the socket from a stream.  Reconfigures the stream so that
*  closing the stream doesn't close the socket. 
*  \param readStream The stream from which to extract the socket.
*  \param sockPtr A pointer to a socket; the pointer itself must not be NULL;
*  on entry, the pointed-to value is ignored; on success, it's set to the newly-created
*  socket; on error the value is unchanged.
*  \returns On success, 0; on error, an errno-style error number.
*/

static errno_t ExtractSocketFromReadStream(CFReadStreamRef readStream, int * sockPtr) {
    errno_t        err;
    Boolean        success;
    CFDataRef      sockData;

    err = EINVAL;

    // None of this should fail but, if it does, we've set things up so that we leak
    // the socket rather double close the socket.  That is, we set
    // kCFStreamPropertyShouldCloseNativeSocket to false before getting
    // kCFStreamPropertySocketNativeHandle.

    success = CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
    if (success) {
        sockData = CFReadStreamCopyProperty(readStream, kCFStreamPropertySocketNativeHandle);
        if (sockData != NULL) {
            if (CFDataGetLength(sockData) == sizeof(int)) {
                *sockPtr = * (const int *) CFDataGetBytePtr(sockData);
                assert(*sockPtr >= 0);
                err = 0;
            }
            CFRelease(sockData);
        }
    }
    return err;
}

/*! Destroys the supplied read and write streams.
*  \param readStream The read stream to destroy.
*  \param writeStream The read stream to destroy.
*/

static void DestroyStreams(CFReadStreamRef readStream, CFWriteStreamRef writeStream) {
    Boolean    success;

    assert( readStream != NULL);
    assert(writeStream != NULL);

    success = CFReadStreamSetClient(  readStream, 0, NULL, NULL);
    assert(success);
    success = CFWriteStreamSetClient(writeStream, 0, NULL, NULL);
    assert(success);
    CFReadStreamClose(  readStream);
    CFWriteStreamClose(writeStream);
    CFRelease( readStream);
    CFRelease(writeStream);
}

/*! Creates a socket that's connect to the specified port on the specified host.
*  An internal version of SocketConnectedToHostname that takes various parameters
*  in some easier-to-digest formats.
*  \param hostName The host to connect to.
*  \param port The port on that host.
*  \param deadline When a failed connection should time out.
*  \param sockPtr A pointer to a socket; the pointer itself must not be NULL;
*  on entry, the pointed-to value is ignored; on success, it's set to the newly-created
*  socket; on error the value is unchanged.
*  \returns On success, 0; on error, an errno-style error number.
*/

static errno_t SocketConnectedToHostnameInternal(
    CFStringRef            hostName,
    int                    port,
    CFAbsoluteTime          deadline,
    int *                  sockPtr
) {
    errno_t                err;
    int                    sock;
    CFReadStreamRef        readStream;
    CFWriteStreamRef        writeStream;
    SuperConnectContext    context = { kSuperConnectContextMagic, 0, NULL };

    assert(hostName != nil);
    assert(port > 0);
    assert(port < 65536);
    // It's hard to come up with meaningful asserts for deadline.
    assert(sockPtr != NULL);

    sock = -1;

    err = CreateAndOpenStreams(&context, hostName, port, &readStream, &writeStream);
    if (err == 0) {
        RunRunLoopWithDeadline(&context, deadline);

        if (context.openCount == 2) {
            err = ExtractSocketFromReadStream(readStream, &sock);
        } else if (context.error != NULL) {
            if ( CFEqual(CFErrorGetDomain(context.error), kCFErrorDomainPOSIX) ) {
                err = (errno_t) CFErrorGetCode(context.error);
                assert(err != 0);
            } else {
                err = EINVAL;
            }
            CFRelease(context.error);
        } else {
            assert(false);
        }
    }
    DestroyStreams(readStream, writeStream);

    if (err == 0) {
        *sockPtr = sock;
    }

    return err;
}

/*! Returns a socket that's connected to the specified port on the specified host.
*  \param hostName The host to connect to.  This can be an IPv4 or IPv6 address, but
*  it's typically a DNS name.
*  \param serviceName The name of the service on that host to connect to.  It's common
*  to use a number here ("631") but it's also possible to supply a service name ("ipp").
*  \param timeout A timeout; pass NULL for no timeout.
*  \returns On success, this returns a socket.  On error, this returns -1 and the
*  error code is in errno.
*/

extern int SocketConnectedToHostname(
    const char *            hostName,
    const char *            serviceName,
    const struct timeval *  timeout
) {
    int                result;
    errno_t            err;
    CFStringRef        hostStr;
    int                port;
    int                sock;
    CFAbsoluteTime      deadline;

    assert(hostName != NULL);
    assert(serviceName != NULL);
    // duration may be NULL

    err = 0;
    sock = -1;

    // Convert input parameters into formats appropriate for CFNetwork.

    hostStr = CFStringCreateWithCString(NULL, hostName, kCFStringEncodingUTF8);
    if (hostStr == NULL) {
        err = EINVAL;
    }

    if (err == 0) {
        err = PortForService(serviceName, &port);
    }

    if (err == 0) {
        if (timeout == NULL) {
            deadline = DBL_MAX;
        } else {
            deadline = CFAbsoluteTimeGetCurrent() + (CFAbsoluteTime) timeout->tv_sec + ((CFAbsoluteTime) timeout->tv_usec) / 1000000.0;
        }
    }

    // Do the work.

    if (err == 0) {
        err = SocketConnectedToHostnameInternal(hostStr, port, deadline, &sock);
    }

    // Clean up.

    if (hostStr != NULL) {
        CFRelease(hostStr);
    }
    if (err == 0) {
        result = sock;
        assert(result >= 0);
    } else {
        result = -1;
        errno = err;
    }

    return result;
}

Thanks for the code snippet. I just uploaded the sysdiagnose report along with scutil output to the ticket.

Hi Quinn,


Can we do the same thing with CFSocket instead of BSD. We have existing code written in CFSocket but would need support for Happy Eyeballs. So can we connect using (CF/NS)Stream and then once connect is successful get CFSocket out of it and hook our existing working flow? I tried it but the CFSocket callout is not getting called after using "CFSocketCreateWithNative" from the native socket property of the CFStream. Does this approach have any limitation?


Thanks.

I can’t think of anything that would prevent you from extracting the underlying file descriptor from a

CFSocketStream
, closing the stream, and then using that file descriptor with
CFSocket
. However,
CFSocket
is a very wacky API and it’s easy to imagine something going wrong at that level.

If you can’t get this working I recommend that you open a DTS tech support incident and I can look at it in depth in that context; this stuff is just too complex for the amount of time I have available here on DevForums.

A better option would be to remove your

CFSocket
dependency. The one thing that
CFSocket
was useful for, making it easier to run non-TCP sockets asynchronously, has long be supplanted by Dispatch.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

HI Quinn,


I have opened the DTS 693719133 for this.


Thanks.