Querying Nameservers on dual-stack hosts

11. Querying Nameservers on dual-stack hosts #

The already seen RFC 8305 Happy Eyeballs Version 2: Better Connectivity Using Concurrency force the same preference for IPv6 name servers as it does for establishing new connections:

If multiple DNS server addresses are configured for the current network, the client may have the option of sending its DNS queries over IPv4 or IPv6. In keeping with the Happy Eyeballs approach, queries SHOULD be sent over IPv6 first (note that this is not referring to the sending of AAAA or A queries, but rather the address of the DNS server itself and IP version used to transport DNS messages). If DNS queries sent to the IPv6 address do not receive responses, that address may be marked as penalized and queries can be sent to other DNS server addresses.

But unfortunately glibc resolver doesn’t support this logic. From man 5 resolv.conf:

nameserver Name server IP address Internet address of a name server that the resolver should query, either an IPv4 address (in dot notation), or an IPv6 address in colon (and possibly dot) notation as per RFC 2373. Up to MAXNS (currently 3, see <resolv.h>) name servers may be listed, one per keyword. If there are multiple servers, the resolver library queries them in the order listed. If no nameserver entries are present, the default is to use the name server on the local machine. (The algorithm used is to try a name server, and if the query times out, try the next, until out of name servers, then repeat trying all the name servers until a maximum number of retries are made.)

This essentially means using nameservers in order, regardless of which IP stack version they belong to.

If we remember, the musl version of getaddrinfo() sends all requests in parallel to all nameservers. Thus we can say that their implementation is close to the RFC 8305.

Unfortunately, the systemd-resolved also doesn’t do this as well even though it allows to configure DNS servers in a more flexible way:

DNS=

A space-separated list of IPv4 and IPv6 addresses to use as system DNS servers.

Each address can optionally take a port number separated with “:”, a network interface name or index separated with “%”, and a Server Name Indication (SNI) separated with “#”. When IPv6 address is specified with a port number, then the address must be in the square brackets. That is, the acceptable full formats are “111.222.333.444:9953%ifname#example.com” for IPv4 and “[1111:2222::3333]:9953%ifname#example.com” for IPv6. DNS requests are sent to one of the listed DNS servers in parallel to suitable per-link DNS servers acquired from systemd-networkd.service(8) or set at runtime by external applications. For compatibility reasons, if this setting is not specified, the DNS servers listed in /etc/resolv.conf are used instead, if that file exists and any servers are configured in it. This setting defaults to the empty list.

But the code of choosing a DNS server to query is a loop:

DnsServer *manager_get_dns_server(Manager *m) {
        Link *l;
        assert(m);

        /* Try to read updates resolv.conf */
        manager_read_resolv_conf(m);

        /* If no DNS server was chosen so far, pick the first one */
        if (!m->current_dns_server ||
            /* In case m->current_dns_server != m->dns_servers */
            manager_server_is_stub(m, m->current_dns_server))
                manager_set_dns_server(m, m->dns_servers);

        while (m->current_dns_server &&
               manager_server_is_stub(m, m->current_dns_server)) {
                manager_next_dns_server(m, NULL);
                if (m->current_dns_server == m->dns_servers)
                        manager_set_dns_server(m, NULL);
        }

The Nginx allows to specify DNS servers in its config, but the addresses are asked in round-robin order without any preferences for IPv6.

Syntax: 	resolver address ... [valid=time] [ipv4=on|off] [ipv6=on|off] [status_zone=zone];
Default: 	--
Context: 	http, server, location

Configures name servers used to resolve names of upstream servers into addresses, for example:

    resolver 127.0.0.1 [::1]:5353;

The address can be specified as a domain name or IP address, with an optional port (1.3.1, 1.2.2). If port is not specified, the port 53 is used. Name servers are queried in a round-robin fashion. 

The c-ares stub resolver library unfortunately also doesn’t prefer IPv6 over IPv4 DNS server:

ares_status_t ares__send_query(struct query *query, const ares_timeval_t *now)
{
   
  /* Choose the server to send the query to */
  if (channel->rotate) {
    /* Pull random server */
    server = ares__random_server(channel);
  } else {
    /* Pull server with failover behavior */
    server = ares__failover_server(channel);
  }
   
}

Therefore, this is an area open for future improvement.

Read next chapter →