Technical Note TN1145

Living in a Dynamic TCP/IP Environment

This technote describes some of the intricacies of dealing with TCP/IP in a dynamic environment, such as that provided by Mac OS. Specifically, it describes how to write BSD sockets and Open Transport code which correctly handles multi-homing (multiple IP addresses), dial-up links, sleep and wakeup on a PowerBook (or modern desktop), modem disconnection, and user reconfiguration.

This note is directed at all developers who are directly using the networking APIs provided by Mac OS X and traditional Mac OS.

Dynamic TCP/IP Fundamentals
Two Multi-homing Gotchas
Calling All IPs
No Nuisance Calls, Please
Endpoint Mortality
Server Etiquette
Talking to Yourself
Summary
References
Early Revision History
Document Revision History

Dynamic TCP/IP Fundamentals

Many TCP/IP programs make false assumptions about the TCP/IP environment. While assumptions like "this machine has TCP/IP, therefore it must have an IP address" and "the machine's IP address won't change" were valid for workstations on a university Ethernet, they do not hold for a PowerBook running Mac OS connected via PPP. Specifically, the following are non-obvious consequences of Mac OS's dynamic TCP/IP environment:

This technote describes the techniques you must adopt to cope with these consequences. Its primary focus is Mac OS X, although it does cover traditional Mac OS issues as well. The technote covers the solutions to a number of multi-homing gotchas, the various techniques for determining the local IP address list, how to avoid dialing the modem unexpectedly, how to handle changes is the local IP address list, and how to talk to yourself using the loopback IP address.

It does not discuss older TCP/IP APIs or their implementations, such as the MacTCP or Open Transport's MacTCP compatibility. The MacTCP programming interface is insufficient to handle all of the cases described in this note.

This technote also does not cover high-level APIs layered on top of these core networking APIs. For information on how to handle dynamic TCP/IP issues when using high-level networking API, such as URL Access and CFNetwork, see the documentation for the specific high-level API you're using.

Terminology

Mac OS supports a number of networking APIs, and many of those APIs have multiple implementations. Before we can talk specifics, we have to define exactly what we mean by each term:

  • traditional Mac OS — All versions of Mac OS prior to Mac OS X. When used without qualification this also includes Classic.

  • Mac OS X — Mac OS X 10.0 and later.

  • Classic — Traditional Mac OS running inside the Classic compatibility environment on Mac OS X.

  • Mac OS — All versions of Mac OS, including both traditional Mac OS and Mac OS X.

  • MacTCP API — A very old TCP/IP programming interface supported by MacTCP and Open Transport.

  • Open Transport API — The XTI-based TCP/IP programming interface introduced with Open Transport. This API is also supported on Mac OS X, both within the Classic compatibility environment and by a Carbon-based compatibility library.

  • BSD sockets — The native network programming interface on Mac OS X. This is the standard networking API for UNIX. You should take advantage of the numerous non-Apple documents covering BSD sockets, some of which are referenced below.

  • MacTCP — The implementation of the MacTCP API on traditional Mac OS prior to the introduction of Open Transport.

  • Open Transport — A network environment for traditional Mac OS, including the Classic compatibility environment on Mac OS X. Open Transport was first introduced as part of System 7.5.2 on the Power Mac 9500. Open Transport can be installed on systems as old as System 7.1. Open Transport was first included as part of the standard install in System 7.5.3. Open Transport was the default networking stack for Mac OS 7.6 and all subsequent versions of traditional Mac OS.

  • MacTCP compatibility — Open Transport provides MacTCP API compatibility for applications that use the MacTCP API.

  • BSD networking — The core network environment for Mac OS X.

  • Open Transport compatibility library — Mac OS X includes a library that provides Open Transport API compatibility for Carbon-based applications.

Common Code

Many of the code samples in this technote rely on the common routines shown in Listing 1.

Listing 1  Common utility routines

// Error Handling
// --------------
// SCF returns errors in two ways:
//
// o The function result is usually set to something
//   generic (like NULL or false) to indicate an error.
//
// o There is a call, SCError, that returns the error
//   code for the most recent function. These error codes
//   are not in the OSStatus domain.
//
// We deal with this using two functions, MoreSCError
// and MoreSCErrorBoolean. Both of these take a generic
// failure indicator (a pointer or a Boolean) and, if
// that indicates an error, they call SCError to get the
// real error code. They also act as a bottleneck for
// mapping SC errors into the OSStatus domain, although
// I don't do that in this simple implementation.
//
// Note that I could have eliminated the failure indicator
// parameter and just called SCError but I'm worried
// about SCF returning an error indicator without setting
// the SCError. There's no justification for this worry
// other than general paranoia (I know of no examples where
// this happens),

static OSStatus MoreSCErrorBoolean(Boolean success)
{
    OSStatus err;
    int scErr;

    err = noErr;
    if ( ! success ) {
        scErr = SCError();
        if (scErr == kSCStatusOK) {
            scErr = kSCStatusFailed;
        }
        // Return an SCF error directly as an OSStatus.
        // That's a little cheesy. In a real program
        // you might want to do some mapping from SCF
        // errors to a range within the OSStatus range.
        err = scErr;
    }
    return err;
}

static OSStatus MoreSCError(const void *value)
{
    return MoreSCErrorBoolean(value != NULL);
}

static OSStatus CFQError(CFTypeRef cf)
    // Maps Core Foundation error indications (such as they
    // are) to the OSStatus domain.
{
    OSStatus err;

    err = noErr;
    if (cf == NULL) {
        err = coreFoundationUnknownErr;
    }
    return err;
}

static void CFQRelease(CFTypeRef cf)
    // A version of CFRelease that's tolerant of NULL.
{
    if (cf != NULL) {
        CFRelease(cf);
    }
}

Two Multi-homing Gotchas

Multi-homing is the term used to describe a computer with more than one IP address. This section describes how to deal with two common gotchas on multi-homed machines. These techniques are relevant on both traditional Mac OS and Mac OS X.

Most importantly, however, is that all versions of Mac OS X support multi-homing. If you follow the guidelines in this section then your code will work properly on both systems.

Multi-homing and Bind

In order to be compatible with multi-homing, your software should, in general, bind to the wildcard IP address (INADDR_ANY for BSD sockets code, kOTAnyInetAddress for Open Transport code), not to a specific IP address. For outgoing connections this is very simple: for BSD sockets code you should just call connect without calling bind beforehand, and for Open Transport code you should bind your endpoint using the code shown in Listing 2.

Listing 2  Binding an endpoint for outgoing connections

static OSStatus DoOutgoingBind(EndpointRef ep) {     OSStatus err;      err = OTBind(ep, NULL, NULL);     return err; }
  • writing too much code

  • creating software that won't work on a multi-homed machine that is connected to discontiguous internets

  • making it harder to update your software for IPv6

Listing 3 shows how not to bind an endpoint for an outgoing connection.

Listing 3  How not to bind an endpoint for outgoing connections

// ***** DO NOT DO THIS *****

static OSStatus DoOutgoingBindWrongly(EndpointRef ep)
{
    OSStatus            err;
    InetInterfaceInfo   info;
    InetAddress         primaryAddr;
    TBind               bindReq;

    err = OTInetGetInterfaceInfo(&info, kDefaultInetInterface);
    if (err == noErr) {
        OTInitInetAddress(&primaryAddr, 0, info.fAddress);

        OTMemzero(&bindReq, sizeof(bindReq));
        bindReq.addr.buf = (UInt8 *) &primaryAddr;
        bindReq.addr.len = sizeof(primaryAddr);

        err = OTBind(ep, &bindReq, NULL);
    }
    return err;
}

// ***** DO NOT DO THIS *****

For software that accepts incoming connections, the situation is very similar: in most cases you should bind to the wildcard IP address (INADDR_ANY or kOTAnyInetAddress). Code to do this for both BSD sockets and the Open Transport API is shown in Listing 4.

Listing 4  Binding for incoming connections

static int DoIncomingBindBSDSockets(int sock, u_short port)
{
    int err;
    struct sockaddr_in req;

    memset(&req, 0, sizeof(req));
    req.sin_family = AF_INET;
    req.sin_addr.s_addr = htonl(INADDR_ANY);
    req.sin_port = htons(port);

    err = bind(sock, (struct sockaddr *) &req, sizeof(req));
    if (err == -1) {
        err = errno;
    }

    return err;
}

static OSStatus DoIncomingBindOT(EndpointRef ep, InetPort port)
{
    OSStatus    err;
    TBind       req;
    TBind       ret;
    InetAddress reqAddr;
    InetAddress retAddr;

    OTInitInetAddress(&reqAddr, port, kOTAnyInetAddress);

    OTMemzero(&req, sizeof(req));
    req.addr.buf    = (UInt8 *) &reqAddr;
    req.addr.len    = sizeof(reqAddr);
    req.qlen        = 10;

    OTMemzero(&ret, sizeof(ret));
    ret.addr.buf    = (UInt8 *) &retAddr;
    ret.addr.maxlen = sizeof(retAddr);

    err = OTBind(ep, &req, &ret);
    if (err == noErr) {
        if (retAddr.fPort != reqAddr.fPort) {
            (void) OTUnbind(ep);
            err = kOTAddressBusyErr;
        }
    }
    return err;
}

In some circumstances it is useful for server software to bind to a specific IP address. For example, a web server might do this so it can return different web pages depending on the address to which the client connected. The DTS sample code OTSimpleServerHTTP illustrates this technique. However, unless you need to respond differently on different IP addresses, the correct thing to do is to bind your server to the wildcard IP address.

Multi-homing and FTP

On a multi-link multi-homed computer, it's possible for the "correct" local IP address of the computer to depend on the IP address of the host you're communicating with.

For example, consider a computer with two Ethernet cards, one connected to the public Internet and one connected to a private network. Each card has an IP address but the machine does not forward IP packets between the cards. This situation is illustrated Figure 1.

Figure 1  An example multi-homed configuration

Now consider an FTP client running on this computer. One of the (mis)features of the FTP protocol is that, when it opens a data connection, the FTP client must open a port on the local machine and send (through the control connection) the port number and the local IP address to the server in a PORT command. However, machines on the public Internet can only communicate with 17.100.55.7, and machines on the private network can only communicate with 17.200.37.41. So how does the FTP client work out which IP address to send?

The answer is that each networking API has a function that returns the source local IP address for a connection. For BSD sockets this is getsockname and for the Open Transport API it is OTGetProtAddress. So the FTP client can call one of these functions on the control connection to determine the correct local address to use for the data connection.

The code in Listing 5 shows how to get the local address for a connection using both BSD sockets and the Open Transport API.

Listing 5  Getting the local IP address for a connection

static int GetLocalIPAddressForConnectionBSDSockets(int sock,
                                    struct in_addr *localAddr)
{
    int err;
    int len;
    struct sockaddr_in local;

    len = sizeof(local);
    err = getsockname(sock, (struct sockaddr *) &local, &len);
    if (err == -1) {
        err = errno;
    }
    if (err == 0) {
        assert(len == sizeof(local));
        *localAddr = local.sin_addr;
    }

    return err;
}

static OSStatus GetLocalIPAddressForConnectionOT(EndpointRef ep,
                                         InetAddress *localAddr)
{
    OSStatus err;
    TBind localBind;

    // only works if endpoint connected
    assert(OTGetEndpointState(ep) == T_DATAXFER);
    assert(OTIsBlocking(ep));

    OTMemzero(&localBind, sizeof(TBind));
    localBind.addr.buf = (UInt8 *) localAddr;
    localBind.addr.maxlen = sizeof(InetAddress);

    err = OTGetProtAddress(ep, &localBind, NULL);
    return err;
}

Calling All IPs

This section describes how you can get a list of all of the IP addresses on the machine and be notified when that list changes.

If you're using this code for any other reason you should carefully consider the multi-homing compatibility issues before proceeding.

If you're writing a server that responds differently depending on which IP address the client connects to, you should also read Server Etiquette.

Getting a List of All IP Addresses

There are at least three different methods for getting a list of all of the machine's IP addresses (the local IP address list):

  • System Configuration framework

  • Open Transport API

  • BSD sockets

The following sections describe each of these methods in turn. The choice of which method to use depends on the system requirements and existing structure of your code:

  • If you need a method that works on traditional Mac OS, you should use the Open Transport API. You can then choose to use the same code on Mac OS X, or write new code for Mac OS X that uses System Configuration framework.

  • If you want to share source code with other UNIX platforms, the BSD sockets approach is probably best.

  • If you're writing code solely for Mac OS X and you need to be notified when the list of IP local addresses changes, System Configuration framework is the best approach.

All other things being equal, Apple recommends the System Configuration framework (SCF) approach over the BSD sockets approach because

  • SCF provides a way to notify you when the list changes, and

  • SCF isolates you from changes in the underlying network kernel.

System Configuration Framework

Mac OS X 10.1 introduced a public API, the System Configuration framework (SCF), for accessing network setup and state information. SCF is the recommended way for you to get the local IP address list under Mac OS X. Listing 6 shows how you can do this.

Listing 6  Getting the local IP address list using System Configuration framework

static void GetIPAddressListFromValue(const void *key,
                                      const void *value,
                                      void *context)
    // This function is a callback CopyIPAddressListSCF when
    // it calls CFDictionaryApplyFunction. It extracts the
    // IPv4 address list from the network service dictionary
    // and appends it to the result dictionary (which is passed
    // in via the context pointers).
{
    CFArrayRef intfAddrList;

    assert( key != NULL );
    assert( CFGetTypeID(key) == CFStringGetTypeID() );
    assert( value != NULL );
    assert( CFGetTypeID(value) == CFDictionaryGetTypeID() );
    assert( context != NULL );
    assert( CFGetTypeID(context) == CFArrayGetTypeID() );

    intfAddrList = CFDictionaryGetValue(value,
                            kSCPropNetIPv4Addresses);
    if (intfAddrList != NULL) {
        assert( CFGetTypeID(intfAddrList)
                == CFArrayGetTypeID() );
        CFArrayAppendArray(context,
                            intfAddrList,
                            CFRangeMake(0, CFArrayGetCount(intfAddrList))
                            );
    }
}

static OSStatus CopyIPAddressListSCF(CFArrayRef *addrList)
    // Returns a CFArray that contains every IPv4
    // address on the system (as CFStrings) in no
    // particular order.
{
    OSStatus            err;
    SCDynamicStoreRef   ref;
    CFStringRef         pattern;
    CFArrayRef          patternList;
    CFDictionaryRef     valueDict;
    CFMutableArrayRef   result;

    assert( addrList != NULL);
    assert(*addrList == NULL);

    ref         = NULL;
    pattern     = NULL;
    patternList = NULL;
    valueDict   = NULL;
    result      = NULL;

    // Create a connection to the dynamic store, then create
    // a search pattern that finds all IPv4 entities.
    // The pattern is "State:/Network/Service/[^/]+/IPv4".

    ref = SCDynamicStoreCreate( NULL,
                                CFSTR("CopyIPAddressListSCF"),
                                NULL,
                                NULL);
    err = MoreSCError(ref);
    if (err == noErr) {
        pattern = SCDynamicStoreKeyCreateNetworkServiceEntity(
                                NULL,
                                kSCDynamicStoreDomainState,
                                kSCCompAnyRegex,
                                kSCEntNetIPv4);
        err = MoreSCError(pattern);
    }

    // Now make a pattern list out of the pattern and then
    // call SCDynamicStoreCopyMultiple. We use that call,
    // rather than repeated calls to SCDynamicStoreCopyValue,
    // because it gives us a snapshot of the state.

    if (err == noErr) {
        patternList = CFArrayCreate(NULL,
                                    (const void **) &pattern,
                                    1,
                                    &kCFTypeArrayCallBacks);
        err = CFQError(patternList);
    }
    if (err == noErr) {
        valueDict = SCDynamicStoreCopyMultiple(ref,
                                               NULL,
                                               patternList);
        err = MoreSCError(valueDict);
    }

    // For each IPv4 entity that we found, extract the list
    // of IP addresses and append it to our results array.

    if (err == noErr) {
        result = CFArrayCreateMutable(NULL, 0,
                                      &kCFTypeArrayCallBacks);
        err = CFQError(result);
    }

    // Iterate over the values, extracting the IP address
    // arrays and appending them to the result.

    if (err == noErr) {
        CFDictionaryApplyFunction(valueDict,
                                  GetIPAddressListFromValue,
                                  result);
    }

    // Clean up.

    CFQRelease(ref);
    CFQRelease(pattern);
    CFQRelease(patternList);
    if (err != noErr && result != NULL) {
        CFQRelease(result);
        result = NULL;
    }
    *addrList = result;

    assert( (err == noErr) == (*addrList != NULL) );

    return err;
}

Open Transport API

The code in Listing 7 shows how to determine the local IP address list using Open Transport calls. This code will work on both traditional Mac OS and Mac OS X. If your program runs on traditional Mac OS, this is the code to use. If your code code also runs on Mac OS X you can continue to use this code, although Apple recommend that you use System Configuration framework because that provides a way to notify you when the list changes.

Listing 7  Getting the local IP address list using Open Transport

typedef InetHost **InetHostHandle;

static OSStatus AddSecondaryAddresses(
                        InetInterfaceInfo* interfaceInfo,
                        SInt32 interfaceIndex,
                        InetHostHandle addrList)
{
    OSStatus err;
    InetHost *addrBuf;
    UInt32   addrCount;

    addrBuf = NULL;

    addrCount = interfaceInfo->fIPSecondaryCount;

    // Allocate a buffer for the secondary address info.

    addrBuf = (InetHost *) NewPtr(addrCount * sizeof(InetHost));
    if (addrBuf == NULL) {
        err = kENOMEMErr;
    }

    // Ask OT for the list of secondary addresses on this
    // interface and add each secondary address to the list.

    if (err == noErr) {
        err = OTInetGetSecondaryAddresses(addrBuf,
                                          &addrCount,
                                          interfaceIndex);
    }
    if (err == noErr) {
        err = PtrAndHand(addrBuf,
                         (Handle) addrList,
                         addrCount * sizeof(InetHost));
    }

    // Clean up.

    if (addrBuf != NULL) {
        DisposePtr( (Ptr) addrBuf );
    }

    return err;
}

enum
{
    kOTIPSingleLinkMultihomingVersion = 0x01300000
};

static OSStatus GetIPAddressListOT(InetHostHandle addrList)
{
    OSStatus            err;
    Boolean             haveIPSingleLinkMultihoming;
    NumVersionVariant   otVersion;
    SInt32              interfaceIndex;
    InetInterfaceInfo   info;
    Boolean             done;

    // Must be running at system task time.
    assert( TaskLevel() == 0 );

    SetHandleSize( (Handle) addrList, 0 );
    assert( MemError() == noErr );

    haveIPSingleLinkMultihoming =
        ( Gestalt(gestaltOpenTptVersions, (long *) &otVersion) == noErr
        && (otVersion.whole >= kOTIPSingleLinkMultihomingVersion )
        && ( OTInetGetSecondaryAddresses
                         != (void *) kUnresolvedCFragSymbolAddress));

    err = noErr;
    done = false;
    interfaceIndex = 0;
    do {
        done = ( OTInetGetInterfaceInfo(&info, interfaceIndex) != noErr );
        if ( ! done ) {

            // If all interfaces are disabled Mac OS X
            // returns a single interface with a 0
            // address, so we specifically exclude that.
            // Otherwise just add the IP address of this
            // interface to the list.

            if (info.fAddress != kOTAnyInetAddress) {
                err = PtrAndHand(&info.fAddress,
                                 (Handle) addrList,
                                 sizeof(InetHost));
            }

            // Now add any secondary addresses.

            if ( err == noErr
                 && haveIPSingleLinkMultihoming
                 && info.fIPSecondaryCount > 0 ) {
                err = AddSecondaryAddresses(&info,
                                            interfaceIndex,
                                            addrList);
            }
            interfaceIndex += 1;
        }
    } while (err == noErr && !done);

    return err;
}

BSD Sockets

Mac OS 10.2 and later support a new call, getifaddrs, that returns a list of network interfaces and their assigned addresses. This is the easiest way to get the local IP address list using BSD-style APIs. However, this only works on Mac OS X 10.2 and later. In earlier systems you must dig a little deeper.

The BSD sockets API provides an ioctl, SIOCGIFCONF, that returns a list of all the interfaces on the system. You can use this ioctl to get the local IP address list. An example of doing this is the get_ifi_info routine on page 434 of UNIX Network Programming. You can even download the source code from the book's web page. This technique is useful on Mac OS X 10.1.x and earlier, and if portability to other UNIX platforms is a requirement.

When Good IPs Go Bad

If you cache the local IP address list (either implicitly, by opening a listener for each local IP address, or explicitly in your code), you must implement a mechanism to update your cache when the list changes. The best way to do this depends on your target platform: techniques for both Mac OS X and traditional Mac OS are described below. If you have a Carbon application that runs on both platforms you will probably need to use both techniques.

Mac OS X

On Mac OS X the best way to be notified of changes in the local IP address list is to request notifications from System Configuration framework. Listing 8 shows how to set this up. This function creates a connection to the System Configuration framework dynamic store and a corresponding run loop source. If you add (using CFRunLoopAddSource) this run loop source to your run loop (typically found with CFRunLoopGetCurrent), whatever function you supplied to CreateIPAddressListChangeCallbackSCF will be called when any IPv4 entities change in the System Configuration framework dynamic store, that is, when the local IP address list changes.

Listing 8  Using System Configuration framework for IP address change notification

static OSStatus CreateIPAddressListChangeCallbackSCF(
                        SCDynamicStoreCallBack callback,
                        void *contextPtr,
                        SCDynamicStoreRef *storeRef,
                        CFRunLoopSourceRef *sourceRef)
    // Create a SCF dynamic store reference and a
    // corresponding CFRunLoop source. If you add the
    // run loop source to your run loop then the supplied
    // callback function will be called when local IP
    // address list changes.
{
    OSStatus                err;
    SCDynamicStoreContext   context = {0, NULL, NULL, NULL, NULL};
    SCDynamicStoreRef       ref;
    CFStringRef             pattern;
    CFArrayRef              patternList;
    CFRunLoopSourceRef      rls;

    assert(callback   != NULL);
    assert( storeRef  != NULL);
    assert(*storeRef  == NULL);
    assert( sourceRef != NULL);
    assert(*sourceRef == NULL);

    ref = NULL;
    pattern = NULL;
    patternList = NULL;
    rls = NULL;

    // Create a connection to the dynamic store, then create
    // a search pattern that finds all IPv4 entities.
    // The pattern is "State:/Network/Service/[^/]+/IPv4".

    context.info = contextPtr;
    ref = SCDynamicStoreCreate( NULL,
                                CFSTR("AddIPAddressListChangeCallbackSCF"),
                                callback,
                                &context);
    err = MoreSCError(ref);
    if (err == noErr) {
        pattern = SCDynamicStoreKeyCreateNetworkServiceEntity(
                                NULL,
                                kSCDynamicStoreDomainState,
                                kSCCompAnyRegex,
                                kSCEntNetIPv4);
        err = MoreSCError(pattern);
    }

    // Create a pattern list containing just one pattern,
    // then tell SCF that we want to watch changes in keys
    // that match that pattern list, then create our run loop
    // source.

    if (err == noErr) {
        patternList = CFArrayCreate(NULL,
                                    (const void **) &pattern, 1,
                                    &kCFTypeArrayCallBacks);
        err = CFQError(patternList);
    }
    if (err == noErr) {
        err = MoreSCErrorBoolean(
                SCDynamicStoreSetNotificationKeys(
                    ref,
                    NULL,
                    patternList)
              );
    }
    if (err == noErr) {
        rls = SCDynamicStoreCreateRunLoopSource(NULL, ref, 0);
        err = MoreSCError(rls);
    }

    // Clean up.

    CFQRelease(pattern);
    CFQRelease(patternList);
    if (err != noErr) {
        CFQRelease(ref);
        ref = NULL;
    }
    *storeRef = ref;
    *sourceRef = rls;

    assert( (err == noErr) == (*storeRef  != NULL) );
    assert( (err == noErr) == (*sourceRef != NULL) );

    return err;
}

Traditional Mac OS

On traditional Mac OS the best technique to watch for changes in the local IP address list varies depending on the version of Open Transport. Open Transport 2.5 and higher (present since Mac OS 9) will deliver TCP/IP stack transition events to your application's notifier (registered using OTRegisterAsClient). You can install a notifier and listen for the kOTStackWasLoaded event, after which you can build the local IP address list as described earlier.

Earlier versions of Open Transport do not send these stack transition events. The only good way to detect changes in the local IP address list is to periodically poll the list.

No Nuisance Calls, Please

Mac OS allows the user to configure their computer to dial on demand via a checkbox in the network control panel ("Connect automatically when starting TCP/IP applications"). Your software must be careful not to accidentally dial the modem when the user wasn't expecting it. The best way to do this depends on the target platform. Before we start discussing these techniques in detail, however, we need to define some terms.

Terminology

For the sake of this discussion, it's helpful to define two classes of network activity. A solicited network operation is the result of a direct user command. Solicited operations should trigger dial on demand if necessary. For example, if the user opens a web page in a browser, or checks for email, or starts downloading a file, they expect the modem to dial if necessary.

In contrast, an unsolicited network operation is only indirectly related to a user action, and should not trigger dial on demand. For example, your application's automatic software update feature is almost certainly an unsolicited operation; most users will be annoyed if your software dials their modem just to check for software updates.

One useful technique is to perform unsolicited operations parasitically; when the user connects to the network for other reasons, your software should take advantage of the network connection and perform any pending unsolicited operations. For example, a mail server program could monitor the network state and choose to check for and deliver mail once the user has established a connection for other reasons. You can watch for changes in network connectivity using the techniques described earlier.

Consider the example of an automatic software update program that sits in the background, waiting for the computer to connect to the network. When that happens, it quickly and automatically checks for the presence of a software update. However, it shouldn't start downloading a multi-megabyte archive without asking the user first, otherwise the traffic associated with this unsolicited operation will prevent an idle disconnect.

In many cases classifying an operation as solicited or unsolicited is a judgment call. To continue the previous example, a mail server application could store mail locally and attempt to parasitically deliver it. However, if the user doesn't establish a connection within a reasonable time (say a day), the mail server should probably connect anyway. Ultimately you need to decide how to manage these situations on a case-by-case basis, and then accept feedback from your users. You may even want to provide a user preference to control this behavior.

Finally, it's worth noting that the techniques discussed below only work if the modem is directly connected to the computer. If the computer's connectivity is indirectly run through a modem (for example, the computer is on a home AirPort network and the AirPort Base Station connects to the Internet via its modem), there is very little you can do to avoid nuisance calls.

Mac OS X

Mac OS X provides specific support for avoiding nuisance calls. System Configuration framework exports two functions, SCNetworkCheckReachabilityByAddress and SCNetworkCheckReachabilityByName, which determine whether communicating with a particular site will trigger a modem to dial. Before doing any unsolicited network operation you should call these functions to see if you might annoy the user by dialing the modem. Listing 9 shows an example of how to do this.

Listing 9  Using System Configuration framework to check reachability

static Boolean UnsolicitedAllowedSCF(const char *serverName)
{
    Boolean                     result;
    SCNetworkConnectionFlags    flags;

    // IMPORTANT:
    // To work with CodeWarrior you should set the
    // "enums are always int" option, which the CWPro8
    // Mach-O stationery fails to do.

    assert(sizeof(SCNetworkConnectionFlags) == sizeof(int));

    result = false;
    if ( SCNetworkCheckReachabilityByName(serverName, &flags) ) {
        result =    !(flags & kSCNetworkFlagsConnectionRequired)
                 && (flags & kSCNetworkFlagsReachable);
    }
    return result;
}

One key difference between traditional Mac OS and Mac OS X is that Mac OS X implements dial on demand by monitoring traffic, whereas traditional Mac OS dials the modem when you open a TCP/IP provider. Therefore, on Mac OS X, you should check for network reachability before communicating, whereas on traditional Mac OS you have to check for network reachability before opening a TCP/IP provider. The situation with traditional Mac OS is explained in more detail below.

Traditional Mac OS

When running on traditional Mac OS, your software must be careful to create a TCP/IP provider only when it actually needs to use the network. This is because the act of you opening a TCP/IP provider can cause the modem to dial, even if you don't send any network traffic.

Your application can avoid dialing the modem for unsolicited operations by simply calling OTInetGetInterfaceInfo before starting the network operation. If OTInetGetInterfaceInfo indicates that the TCP/IP stack is loaded, the modem (if any) has already been dialed and your application can safely use the network. Listing 10 shows how to do this.

Listing 10  Using OTInetGetInterfaceInfo to check reachability

static Boolean UnsolicitedAllowedTrad(const char *serverName)
{
    #pragma unused(serverName)
    InetInterfaceInfo info;

    return ( OTInetGetInterfaceInfo(&info,
             kDefaultInetInterface) == noErr );
}

While the above code is very simple, some developers have found it insufficiently rigorous for their taste. For example, the TCP/IP stack might be unloaded but the machine is connected via Ethernet, so opening a TCP/IP provider is unlikely to inconvenience the user. A more rigorous approach is shown in Listing 11. This uses the NSHTCPWillDial (from the DTS sample code "MoreNetworkSetup") to check for reachability. This has the advantage that it covers more cases, at the cost of make the code significantly more complex (once you start looking inside the implementation of NSHTCPWillDial).

Listing 11  Using NSHTCPWillDial to check reachability

static Boolean UnsolicitedAllowedMNS(const char *serverName)
{
    #pragma unused(serverName)
    UInt32 willDial;

    return (NSHTCPWillDial(&willDial) == noErr)
            && (willDial == kNSHTCPDialNo);
}
  • The case is relatively rare. Most users connect the PPP link using dial on demand, which means the TCP/IP stack is loaded when the link connects.

  • Detecting the PPP link status must be done with link-specific code. While it would be relatively easy to cover some common cases (ARA, FreePPP and derivatives), there would be no end to the list of special cases to cover, and some of those cases are both common and tricky (AOL).

NSHTCPWillDial produces a false negative, which is the least inconvenient failure for the user.

Endpoint Mortality

There are a number of situations where the TCP/IP stack is forced to invalidate some of your connections unexpectedly. The section describes how this can happen and how you can detect and rectify the problem.

Endpoint Mortality on Mac OS X

The TCP/IP stack on Mac OS X is always loaded. This prevents a number of problems that occur on traditional Mac OS. For example, Mac OS X always allows TCP/IP applications to talk to other applications on the same machine (see Talking to Yourself) and never closes Open Transport providers for you. However, when the local IP address list changes, it's possible for the source address of a connection to disappear from the list. This creates a stale connection, one that is effectively broken. Unfortunately, your application is not notified of this. Specifically, you might expect a TCP connection to break (for example, an asynchronous OT endpoint would receive a T_DISCONNECT event), but this doesn't happen. Instead the connection continues to exist but won't be able to send or receive any data.

This problem is fundamental to the TCP/IP protocol itself, and affects both BSD sockets and Open Transport endpoints. A connection is stale if its local address is no longer in the local IP address list. The most commonly affected connections are:

  • connected TCP sockets and endpoints

  • connected UDP sockets

  • non-connected TCP and UDP sockets and endpoints that are bound to a specific IP address (rather than the wildcard IP address)

Your strategy for dealing with stale connections will depend on the nature of your application. For client applications, where connections are typically short-lived, you probably don't need to do anything. If you implement a timeout mechanism the total lack of any data transferred on the connection will eventually trigger your timeout and you will close the connection. Alternatively, the user may choose to manually close the connection.

For client applications with long-lived connections, you should implement some sort of safeguard against stale connections. One technique for doing this is to watch for changes to the local IP address list (using System Configuration framework, as described above) and, after each change, verify that the source address of each of your connections (which you can get using the code shown earlier) is still in the local IP address list. If the connection's source address is no longer in the local IP address list, the connection is stale and you should reestablish it (subject to any nuisance call considerations). Listing 12 shows how to check for stale connections under both BSD sockets and the Open Transport API.

Listing 12  Checking if a connection is stale

static Boolean IsEndpointStale(EndpointRef ep,
                               InetHostHandle addrList)
{
    Boolean     stale;
    InetAddress sourceAddr;
    ItemCount   index;
    ItemCount   count;

    if ( GetLocalIPAddressForConnectionOT(ep,
                                    &sourceAddr) != noErr ) {
        stale = true;
    } else {
        assert(sourceAddr.fAddressType == AF_INET);

        if (sourceAddr.fHost == kOTAnyInetAddress) {
            stale = false;
        } else {
            stale = true;
            count = GetHandleSize( (Handle) addrList ) / sizeof(InetHost);
            for (index = 0; index < count; index++) {
                if ( sourceAddr.fHost == (*addrList)[index] ) {
                    stale = false;
                    break;
                }
            }
        }
    }

    return stale;
}

static Boolean IsSocketStale(int sock, CFArrayRef addrList)
{
    Boolean         stale;
    struct in_addr  sourceAddr;
    CFStringRef     sourceAddrStr;

    sourceAddrStr = NULL;

    if ( GetLocalIPAddressForConnectionBSDSockets(
                                        sock,
                                        &sourceAddr) != 0 ) {
        stale = true;
    } else {
        if (sourceAddr.s_addr == INADDR_ANY) {
            stale = false;
        } else {
            stale = true;

            sourceAddrStr = CFStringCreateWithCString(
                                        NULL,
                                        inet_ntoa(sourceAddr),
                                        kCFStringEncodingASCII);
            if (sourceAddrStr != NULL) {
                stale = ! CFArrayContainsValue(
                                 addrList,
                                 CFRangeMake(0, CFArrayGetCount(addrList)),
                                 sourceAddrStr);
            }
        }
    }

    CFQRelease(sourceAddrStr);

    return stale;
}

For server applications, you should follow the advice in Server Etiquette.

Endpoint Mortality on Traditional Mac OS

The life cycle of the TCP/IP stack on traditional Mac OS is much more complex. A key aspect of Open Transport is that the TCP/IP stack can load and unload from memory. When the stack is unloaded you can't use TCP/IP services. If you create a TCP/IP provider, Open Transport loads the TCP/IP stack. If the computer is configured to dial on demand, then loading the TCP/IP stack will trigger the modem to dial. If the modem connection fails, the TCP/IP stack will fail to load and, if it was triggered by you creating a provider, the provider creation function will return an error.

Techniques for avoid dial on demand problems are discussed in No Nuisance Calls, Please.

The TCP/IP stack can unload from memory in a number of circumstances:

  • if TCP/IP is using the modem and the modem disconnects

  • if the machine's IP address changes (for example, DHCP could not renew a lease)

  • if the computer is put to sleep (except as noted below)

  • if the user modifies the settings in the TCP/IP control panel

  • if some application commits changes to the network settings using Network Setup

  • older versions of Open Transport can unload the TCP/IP stack after two minutes if all providers are closed

When the TCP/IP stack unloads, it closes all TCP/IP providers. When it closes a provider in this way, Open Transport calls the provider's notifier with one of two events.

  • kOTProviderWillClose is sent when OT is closing your provider in a controlled fashion, typically when the user reconfigures the TCP/IP stack. It is always sent at system task time. You may choose to put the provider in synchronous mode and shut down the network connection cleanly.

  • kOTProviderHasClosed is sent when OT "force closes" your provider. This is typically done when the underlying link layer shuts down. It may be sent at system task or deferred task time, so you must write your code to assume it's running at deferred task time. The underlying provider has already been closed but you must call OTCloseProvider to avoid a (small) memory leak.

Regardless of the action of your notifier, the provider will always be closed when you return from your notifier. Any operations on the provider after your notifier returns will return kOTBadReferenceErr.

For TCP/IP providers on which you're actively working (for example, worker endpoints which are actively transferring data), you can ignore these events. The next time you use the provider, you will get a kOTBadReferenceErr error, which your generic error handling should respond to by closing the provider. However, if you open a TCP/IP provider which you're not actively working on (such an InetSvcRef which you use periodically for DNS lookups, or a listening endpoint in a server), you must install a notifier which handles these events. Listing 13 is an example of how to do this.

Listing 13  Handling provider closed events

static InetSvcRef gInetServices = kOTInvalidProviderRef;

static pascal void MyInetServicesNotifier(
        void* contextPtr,
        OTEventCode code,
        OTResult result,
        void* cookie)
{
    #pragma unused(contextPtr)
    #pragma unused(result)
    #pragma unused(cookie)
    switch (code) {
        case kOTSyncIdleEvent:
            // [... yielding code removed ...]
            break;

        case kOTProviderWillClose:
        case kOTProviderIsClosed:
            // OT is closing the provider out from underneath us.
            // We remove our reference to it so the next time
            // someone calls MyStringToAddress, we'll reopen it.
            (void) OTCloseProvider(gInetServices);
            gInetServices = kOTInvalidProviderRef;
            break;
        default:
            // do nothing
            break;
    }
}

static OSStatus MyOpenInetServices(void)
{
    OSStatus err;
    OSStatus junk;

    gInetServices = OTOpenInternetServicesInContext(
        kDefaultInternetServicesPath, 0, &err, NULL);
    if (err == noErr) {
        junk = OTSetBlocking(gInetServices);
        assert(junk == noErr);
        junk = OTSetSynchronous(gInetServices);
        assert(junk == noErr);
        junk = OTInstallNotifier(gInetServices,
            MyInetServicesNotifier, NULL);
        junk = OTUseSyncIdleEvents(gInetServices, true);
        assert(junk == noErr);
    }
    return err;
}

static OSStatus MyStringToAddress(const char *string,
        InetHost *address)
{
    OSStatus err;
    InetHostInfo hostInfo;

    // If the DNS provider isn't currently open, open it.

    err = noErr;
    if (gInetServices == kOTInvalidProviderRef) {
        err = MyOpenInetServices();
    }

    // Now do the name to address translation using the provider.

    if (err == noErr) {
        err = OTInetStringToAddress(gInetServices,
                 (char *) string, &hostInfo);
    }
    if (err == noErr) {
        // For this example, we just return the host's first
        // IP address.
        *address = hostInfo.addrs[0];
    }
    return err;
}

Listing 13 illustrates two important points:

  • The gInetServices provider is not created until the program needs to convert a name to an address. This lazy creation prevents the program dialing the modem prematurely.

  • When the notifier is informed that the provider has closed, it invalidates gInetServices. The next time MyStringToAddress is called, it will notice this and recreate the provider.

Because Open Transport always closes any providers before changing the local IP address list, it's not possible to get stale connections on traditional Mac OS.

Server Etiquette

Handling the events described in the previous section is especially important for servers. A server will typically have one or more open listeners (listening sockets for BSD sockets code, or listening endpoints for Open Transport code), which are waiting for connections from clients. It's important that those listeners remain active regardless of network reconfigurations.

Listeners on Mac OS X

If you follow the advice given earlier, handling listeners on Mac OS X is very simple. The TCP/IP stack on Mac OS X never unloads, so your listeners will never automatically close. Moreover, because your listeners are bound to the wildcard IP address (INADDR_ANY for BSD sockets code, kOTAnyInetAddress for Open Transport code), changes to the local IP address list will never create a stale listener.

If you have bound your listener to a specific IP address (the only good reason for doing this is so that your server can behave differently depending on what IP address the client connects to), you must watch for changes in the local IP address list. If an IP address disappears from the list, you should close its listener. If a new IP address shows up on the list, you should open a new listener for that IP address. Alternatively, if you detect any change to the local IP address list you can simply shut down all of your listeners and restart them based on the new IP address list.

These recommendations apply regardless of whether you're using BSD sockets or the Open Transport API.

Listeners on Traditional Mac OS

If your server has open listeners and the TCP/IP stack unloads, those listeners will close, and your notifier will receive an event to this effect. If you don't handle these events properly, chances are that your server will keep running just fine, except it will receive no more T_LISTEN events and will be "deaf" to its clients.

The standard response to the closing of a listener is to simply reopen the listener. You must not attempt to do this directly in your notifier. Instead you should set a flag that causes your main event loop to reopen the listener as soon as TCP/IP has loaded. You can determine this by checking the result of OTInetGetInterfaceInfo, as described in No Nuisance Calls, Please.

Typically, opening a listener is an unsolicited operation and should be deferred if it would cause the modem to dial. If your application is likely to be used in an environment where it should dial the modem to open a listener, you may want to provide a user preference for this. An example is shown below. Typically the first option would be the default.

Figure 2  Modem control preference for a server program.

Talking to Yourself

Talking to yourself may be the first sign of madness, but TCP/IP software often wants to talk to other software running on the same machine. For example, you might have a server administration tool that configures your server over the network. It's reasonable for a user to run both the server and the administration tool on the same machine.

Both traditional Mac OS and Mac OS X support this sort of TCP/IP loopback, including the standard 127.0.0.1 loopback address. However, there are a couple of caveats, depending on your system version.

Loopback on Mac OS X

The Mac OS X TCP/IP stack is always loaded, and thus the loopback address is always available. Communicating via the loopback address will never trigger a nuisance call on Mac OS X.

The only caveat with loopback on Mac OS X involves Classic. The 127.0.0.1 loopback address works between all programs running in non-Classic environments (Carbon, Cocoa, BSD, Java) and between all programs running in Classic, but doesn't work between non-Classic and Classic programs. Thus, if you have a server running in Classic, you can't connect to it using 127.0.0.1 from a non-Classic application, and vice versa.

The workaround is to not use 127.0.0.1 and actually connect to the shared IP address. Of course, trying to get this shared IP address can cause other problems, as described at the beginning of this technote.

Loopback on Traditional Mac OS X

Loopback on traditional Mac OS is not nearly as straightforward as it is on Mac OS X. Problems occur when the machine is configured for a dial-up connection. Remember that on traditional Mac OS opening a TCP/IP provider causes the TCP/IP stack to load, and loading the TCP/IP stack may cause the modem to dial. This is true even if you're just using the endpoint to talk to yourself. There isn't really a good workaround to this problem.

One less-than-ideal workaround is to reconfigure TCP/IP to not connect via the modem. If no other suitable link is available, you can always configure TCP/IP to "MacIP" (with manual addressing) and configure AppleTalk to "Remote Only". Figure 3 shows what this might look like.

Figure 3  Configuring Open Transport for loopback.

It is possible to use the Network Setup library to programmatically create and switch to these settings.

Summary

TCP/IP is no longer limited to desktop machines on Ethernet. Your code must not make false assumptions about the TCP/IP environment, and must adapt to radical changes in the TCP/IP environment as the user reconfigures and relocates their computer. Your code must also strive to avoid annoying users with dial-up connections by dialing their modem unexpectedly. The information in this technote will help you write software that is a pleasure to use in a dynamic TCP/IP environment.

References

Early Revision History

Table 1  Early revision history

Date

Notes

1998-11-01

Originally written.

2000-01-02

Updated to fix some problems in the sample code, reference MoreNetworkSetup instead of OTTCPWillDial, document an edge case in the nuisance call avoidance algorithm used by MoreNetworkSetup, and reference the stack loading and unloading notifications introduced in Open Transport 2.5.

2000-11-17

Updated to expand the IP address discussion, including specific hints about how to bind endpoints.

2002-08-22

A significant update to cover Mac OS X issues, including BSD sockets, the Open Transport compatibility library, and System Configuration framework.



Document Revision History


DateNotes
2011-07-27

Reformatted content and made minor editorial changes.

2002-08-27

New document that describes some of the intricacies of dealing with TCP/IP in a dynamic environment, such as Mac OS X.