How Node.js is helping you survive the IPv6 era

How Node.js is helping you survive the IPv6 era

·

5 min read

By Paolo Insogna

We take a look at Happy Eyeballs, a crucial tool in ensuring seamless connectivity for users

Every day, pretty much everybody takes for granted the incredible amount of work and technology that makes the internet function. Even developers and other technically-minded individuals often overlook or disregard the intricate details and tasks involved in establishing a connection to a remote host. Let's take a look at one of the details, which happens to have a rather fancy name: Happy Eyeballs.

First of all, what is your address?

When an application seeks to establish a connection to a remote host, it typically only has the hostname. A simple verification of this is that you probably have never typed 216.58.205.46 into your browser's address bar.

Upon receiving the connection request, the browser performs a Domain Name System (DNS) query to resolve the IP address corresponding to the hostname. This information is crucial because certain server application optimisations, such as load balancing, rely on it. Each DNS query can receive its own response, enabling the server to efficiently route clients based on factors like load and geographical position.

Once the client receives an IP address, it has all the necessary components to create a socket: source address, source port, destination address, and destination port.

IPv6 is the future

One detail often overlooked is the IP protocol version. The most well-known and widely supported version is IPv4, where addresses are comprised of four numbers ranging from 0 to 255.

IPv4 has been in use since the 1980s and is still the most commonly used version. However, we're running out of IPv4 addresses. IPv4 theoretically supports only 4 billion addresses, but in practice, due to various technical reasons, the number is much lower.

To address this issue, IPv6 was introduced in 1998 as the successor to IPv4. IPv6 offers a staggering number of addresses ($3.8 \cdot 10^{38}$, to be precise) ensuring we'll never run out. Despite gaining traction in recent years, IPv4 remains the preferred choice, especially for Internet Service Providers and router manufacturers.

This preference can pose a problem; if a destination host only exposes an IPv6 address via DNS and your router or ISP isn't properly configured for IPv6, you may be unable to connect. Similarly, if both IPv4 and IPv6 addresses are provided via DNS but the IPv6 address is listed first, some frameworks may attempt only the first address, resulting in connection failures. This was the case for Node.js before version 19.3.0.

Happy Eyeballs

To address the challenges associated with misconfigured IPv6 connectivity stacks, the Happy Eyeballs RFC (Request for Comments) was introduced in 2012. The whimsical name stems from the term "eyeball," which refers to endpoints representing human internet end-users. The RFC aimed to ensure users wouldn't notice any difference when connecting to a host via IPv6 compared to IPv4.

In 2017, a new version of the RFC was released, proposing a clever solution to the connection problem. If a DNS query returns multiple A (IPv4) and AAAA (IPv6) records, the client must sort them in an alternate order: IPv4, then IPv6, then IPv4 again, and so forth. The code snippet below shows how the sorting is currently done in Node.js.

/*
 validAddress is a tuple of two arrays. 
 Each of them contains either IPv4 or IPv6 only.
 The order of the addresses in the array is the same of DNS response.

 If the first address in DNS response contained a IPv4 address, then
 validAddresses[0] will contain IPv4 only, otherwise is the other way round.
*/
const toAttempt = [];
for (let i = 0, l = MathMax(validAddresses[0].length, validAddresses[1].length); i < l; i++) {
  if (i in validAddresses[0]) {
    ArrayPrototypePush(toAttempt, validAddresses[0][i]);
  }
  if (i in validAddresses[1]) {
    ArrayPrototypePush(toAttempt, validAddresses[1][i]);
  }
}

The client then attempts a connection to the first IP and subsequently to the second IP after a short interval. Importantly, the client doesn't wait for a connection attempt to succeed; once any attempt succeeds, the connection is established, and all other attempts are abandoned.

This approach ensures that regardless of how the DNS records are returned, the client will always be able to choose the best-supported stack.

What about Node.js?

As mentioned earlier, Node.js lacked support for Happy Eyeballs until version 19.3.0, when the autoSelectFamily option was added to net.connect (and made the default in Node.js 20). Prior to this, Node.js would only attempt to connect to the first resolved IPs, regardless of the family, resulting in numerous connection failures.

Due to the internals of Node, Happy Eyeballs was implemented in a loose manner. Although there's never more than one connection attempt to a host, addresses are still attempted in alternate order and with a short, configurable timeout (defaulting to 250ms). This ensures adherence to the RFC's core idea, and subsequently, Node.js is able to correctly choose the right IP to establish a connection. The snippet below shows how Node.js handles the connection timeouts during the Happy Eyeballs algorithm.

function internalConnectMultipleTimeout(context, req, handle) {
  debug('connect/multiple: connection to %s:%s timed out', req.address, req.port);
  context.socket.emit('connectionAttemptTimeout', req.address, req.port, req.addressType);

  req.oncomplete = undefined;
  ArrayPrototypePush(context.errors, createConnectionError(req, UV_ETIMEDOUT));
  handle.close();

  // Try the next address, unless we were aborted
  if (context.socket.connecting) {
    internalConnectMultiple(context);
  }
}

Conclusion

Navigating a dual-stack world where IPv4 and IPv6 coexist requires careful handling, especially in resolving host addresses and establishing connections.

Happy Eyeballs, with its intelligent approach to address resolution and connection attempts, serves as a crucial tool in ensuring seamless connectivity for users, irrespective of the underlying IP protocol version.

With its adoption in frameworks like Node.js, the internet continues to evolve, maintaining its accessibility and reliability for all users.