Search code examples
cnetwork-programmingipv6ipv4getaddrinfo

getaddrinfo() returns only ipv6 when using AF_UNSPEC


When i want to connect to a server (running locally) and don't know if the application uses ipv4, ipv6 or both, should I call getaddrinfo() twice, once with AF_INET and once with AF_INET6, and try all returned addresses?

Some context: getaddrinfo() provides a way for ipv4/ipv6-agnostic hostname resolution. Online i found guidelines stating a common algorithm to connect to a server is to use getaddrinfo() with an AF_UNSPEC hint, and try addresses returned in the list.

However, in my setup, getaddrinfo() only returns an ipv6 entry when I use AF_UNSPEC, the host being "localhost". On the other hand, if I ask for IPv4 explicitly, getaddrinfo() does return an IPv4 address.

This example calls getaddrinfo() once with AF_UNSPEC and once with AF_INET, and iterates over the returned list and prints the address families of the entries:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>

const char* family_to_string(int family) {
    switch (family) {
        case AF_INET:
            return "AF_INET";
        case AF_INET6:
            return "AF_INET6";
        case AF_UNSPEC:
            return "AF_UNSPEC";
        default:
            return "UNKNOWN";
    }
}

int main(void) {
    struct addrinfo hints;
    struct addrinfo *res, *it;
    static const char* host = "localhost";
    static const char* port = "42420";

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = 0;
    hints.ai_family = AF_UNSPEC;
    printf("getaddrinfo(.. %s ..):\n", family_to_string(hints.ai_family));
    int ret = getaddrinfo(host, port, &hints, &res);
    if (ret != 0) {
        return 1;
    }

    for (it = res; it != NULL; it = it->ai_next) {
        printf("entry for %s\n", family_to_string(it->ai_family));
    }
    printf("\n");
    freeaddrinfo(res);

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = 0;
    hints.ai_family = AF_INET;
    printf("getaddrinfo(.. %s ..):\n", family_to_string(hints.ai_family));
    ret = getaddrinfo(host, port, &hints, &res);
    if (ret != 0) {
        return 1;
    }

    for (it = res; it != NULL; it = it->ai_next) {
        printf("entry for %s\n", family_to_string(it->ai_family));
    }
    printf("\n");
    freeaddrinfo(res);

    return 0;
}

It surprised me quite a bit when I received this output:

getaddrinfo(.. AF_UNSPEC ..):
entry for AF_INET6

getaddrinfo(.. AF_INET ..):
entry for AF_INET

After digging a little into the behavior of getaddrinfo(), it appears that it first checks for entries in /etc/hosts, and if it finds matching entries, it only returns those, and does not try to resolve the hostname differently.

Since my /etc/hosts file only contains an ipv6 entry for localhost, only that is returned for AF_UNSPEC. For AF_INET, the entry is not considered eligible and localhost is correctly resolved to 127.0.0.1.


Solution

  • This is indeed an interesting question:

    This is a case, which is specially handled by the nss-files module of glibc; if a request for localhost with address family AF_INET is made and the v6 localhost entry of the /etc/hosts is parsed, it is implicitly converted to the v4 localhost entry with address 127.0.0.1.

    See nss/nss_files/files-hosts.c (around line 70), where this conversion is performed:

    else if (IN6_IS_ADDR_LOOPBACK (entdata->host_addr))
      {
        in_addr_t localhost = htonl (INADDR_LOOPBACK);
        memcpy (entdata->host_addr, &localhost, sizeof (localhost));
      }
    

    This branch is not taken, when asking for AF_UNSPEC, so you will only resolv a v4 localhost address when having an explicit entry in /etc/hosts for it.