1

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.

Ctx
  • 18,090
  • 24
  • 36
  • 51
madmax1
  • 267
  • 1
  • 9

1 Answers1

3

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.

Ctx
  • 18,090
  • 24
  • 36
  • 51
  • Which leads me to the next question: Is the glibc behavior conformant to the POSIX standard? On p. 888 of the standard, it says: "If a specific family is not given and the name could be interpreted as valid within multiple supported families, the implementation shall attempt to resolve the name in all supported families and, in absence of errors, one or more results shall be returned." – madmax1 Apr 11 '19 at 09:50
  • @madmax1 I do not believe that this issue is specified in detail by posix. It probably was a pragmatical fix for some common problem, looks indeed a bit ugly. – Ctx Apr 11 '19 at 09:52
  • Reading the standard, it sounds like it should try to resolve the hostname using **all** protocols. (see comment above) – madmax1 Apr 11 '19 at 09:58
  • 1
    @madmax1 In my eyes, the funny thing is _not_ that a request for `AF_UNSPEC` does not yield a v4 localhost address, but that a request for `AF_INET` _does_. – Ctx Apr 11 '19 at 10:00
  • So do you think i should anticipate a setup in which i cannot reach `"localhost"` via IPv4? – madmax1 Apr 11 '19 at 11:45
  • @madmax1 Sorry, I do not understand what you mean. _Usually_ there is an entry for localhost ipv4 _and_ localhost ipv6 in /etc/hosts, so there shouldn't be an issue at all on a sane system – Ctx Apr 11 '19 at 12:12
  • 2
    @madmax1 IPv6 is the default protocol, and IPv4 is deprecated, so yes, eventually you will find that `localhost` does not work on IPv4. Some OSes (like Windows) explicitly support removing IPv4 entirely, so localhost would _only_ work on IPv6 today. There are already such machines in production. – Michael Hampton Apr 11 '19 at 18:08