8. Stub resolvers in languages #
Let’s now take a look at other popular languages and understand the capabilities, features and options they provide in the context of resolvers.
8.1 Python #
We are going to talk about cpython
3.12.
8.1.1 Stub resolvers #
The Python standard library provides socket.getaddrinfo()
:
socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)
which internally calls libc
getaddrinfo()
:
/* Python interface to getaddrinfo(host, port). */
/*ARGSUSED*/
static PyObject *
socket_getaddrinfo(PyObject *self, PyObject *args, PyObject* kwargs)
{
...
memset(&hints, 0, sizeof(hints));
hints.ai_family = family;
hints.ai_socktype = socktype;
hints.ai_protocol = protocol;
hints.ai_flags = flags;
Py_BEGIN_ALLOW_THREADS
error = getaddrinfo(hptr, pptr, &hints, &res0);
...
}
There are also socket.gethostbyname()
and socket.gethostbyname_ex()
, but they both don’t support IPv6.
The latest significant paradigm shift in Python was the adoption of async functions. The async standard framework includes loop.getaddrinfo()
, which provides asynchronous DNS resolution capabilities:
import asyncio
import socket
async def resolve_hostname(hostname):
loop = asyncio.get_running_loop()
addresses = await loop.getaddrinfo(hostname, None, proto=socket.IPPROTO_TCP)
return addresses
addresses = asyncio.run(resolve_hostname('microsoft.com'))
which internally shows the same behavior as getaddrinfo()
:
strace -f -s0 python3 ./main.py
Open config files:
[pid 49937] openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 6
[pid 49937] openat(AT_FDCWD, "/etc/host.conf", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid 49937] openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 6
[pid 49937] openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 6
[pid 49937] openat(AT_FDCWD, "/etc/gai.conf", O_RDONLY|O_CLOEXEC) = 6
Obtaining source addresses:
[pid 49955] connect(6, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 49955] connect(6, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2603:1030:20e:3::23c", &sin6_addr), sin6_scope_id=0}, 28) = 0
[pid 49955] getsockname(6, {sa_family=AF_INET6, sin6_port=htons(33582), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2001:db8:123:456:6af2:68fe:ff7c:e25c", &sin6_addr), sin6_scope_id=0}, [
28]) = 0
So we can guess that under the hood it still runs socket.getaddrinfo()
:
async def getaddrinfo(self, host, port, *, family=0, type=0, proto=0, flags=0):
if self._debug:
getaddr_func = self._getaddrinfo_debug
else:
getaddr_func = socket.getaddrinfo
return await self.run_in_executor(
None, getaddr_func, host, port, family, type, proto, flags)
The socket.getaddrinfo()
is running in a thread execution pool.
Such a solution could show performance issues under high load, that’s why there are plenty of third party libraries. But I’d like to show the aiodns
. Internally it uses pycares
which is a Python interface for c-ares
.
import aiodns
import asyncio
import socket
async def resolve_hostname(hostname):
resolver = aiodns.DNSResolver()
result = await resolver.gethostbyname(hostname, socket.AF_UNSPEC)
return result
addresses = asyncio.run(resolve_hostname('microsoft.com'))
print(addresses.addresses)
Opening system files:
$ strace -e openat python3 ./dns.py
…
openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY) = 6
openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY) = 6
openat(AT_FDCWD, "/etc/host.conf", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/svc.conf", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/hosts", O_RDONLY) = 6
…
Gather source addresses:
$ strace -f -e trace=network python3 ./dns.py
socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP) = 7
connect(7, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2603:1030:b:3::152", &sin6_addr), sin6_scope_id=0}, 28) = 0
getsockname(7, {sa_family=AF_INET6, sin6_port=htons(58701), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2001:db8:123:456:6af2:68fe:ff7c:e25c", &sin6_addr), sin6_scope_id=0}, [28]) = 0
socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP) = 7
connect(7, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2603:1030:20e:3::23c", &sin6_addr), sin6_scope_id=0}, 28) = 0
getsockname(7, {sa_family=AF_INET6, sin6_port=htons(43168), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2001:db8:123:456:6af2:68fe:ff7c:e25c", &sin6_addr), sin6_scope_id=0}, [28]) = 0
socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP) = 7
connect(7, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2603:1010:3:3::5b", &sin6_addr), sin6_scope_id=0}, 28) = 0
getsockname(7, {sa_family=AF_INET6, sin6_port=htons(36319), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2001:db8:123:456:6af2:68fe:ff7c:e25c", &sin6_addr), sin6_scope_id=0}, [28]) = 0
socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP) = 7
connect(7, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2603:1020:201:10::10f", &sin6_addr), sin6_scope_id=0}, 28) = 0
getsockname(7, {sa_family=AF_INET6, sin6_port=htons(42078), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2001:db8:123:456:6af2:68fe:ff7c:e25c", &sin6_addr), sin6_scope_id=0}, [28]) = 0
socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP) = 7
connect(7, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2603:1030:c02:8::14", &sin6_addr), sin6_scope_id=0}, 28) = 0
getsockname(7, {sa_family=AF_INET6, sin6_port=htons(43403), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2001:db8:123:456:6af2:68fe:ff7c:e25c", &sin6_addr), sin6_scope_id=0}, [28]) = 0
8.1.2 Happy Eyeballs #
The async loop.create_connection()
allows to use the Happy Eyeballs algorithm from RFC 8305. The code is in in Lib/asyncio/base_events.py
:
if happy_eyeballs_delay is None:
# not using happy eyeballs
…
else: # using happy eyeballs
sock, _, _ = await staggered.staggered_race(
(functools.partial(self._connect_sock,
exceptions, addrinfo, laddr_infos)
for addrinfo in infos),
happy_eyeballs_delay, loop=self)
The params of loop.create_connection()
are:
happy_eyeballs_delay
, if given, enables Happy Eyeballs for this connection. It should be a floating-point number representing the amount of time in seconds to wait for a connection attempt to complete, before starting the next attempt in parallel. This is the “Connection Attempt Delay” as defined in RFC 8305. A sensible default value recommended by the RFC is 0.25 (250 milliseconds).
interleave
– controls address reordering when a host name resolves to multiple IP addresses. If 0 or unspecified, no reordering is done, and addresses are tried in the order returned by getaddrinfo(). If a positive integer is specified, the addresses are interleaved by address family, and the given integer is interpreted as “First Address Family Count” as defined in RFC 8305. The default is 0 if happy_eyeballs_delay is not specified, and 1 if it is.
8.2 Go (golang) #
8.2.1 Resolver #
According to the documentation golang has two components to resolve domain names:
On Unix systems, the resolver has two options for resolving names.
It can use a pure Go resolver that sends DNS requests directly to the servers listed in
/etc/resolv.conf
, or it can use a cgo-based resolver that calls C library routines such asgetaddrinfo
andgetnameinfo
.On Unix the pure Go resolver is preferred over the
cgo
resolver, because a blocked DNS request consumes only a goroutine, while a blockedC
call consumes an operating system thread.When
cgo
is available, thecgo
-based resolver is used instead under a variety of conditions: on systems that do not let programs make direct DNS requests (OS X), when theLOCALDOMAIN
environment variable is present (even if empty), when theRES_OPTIONS
orHOSTALIASES
environment variable is non-empty, when theASR_CONFIG
environment variable is non-empty (OpenBSD only), when/etc/resolv.conf
or/etc/nsswitch.conf
specify the use of features that theGo
resolver does not implement.On all systems (except Plan 9), when the
cgo
resolver is being used this package applies a concurrentcgo
lookup limit to prevent the system from running out of system threads. Currently, it is limited to 500 concurrent lookups.The resolver decision can be overridden by setting the
netdns
value of theGODEBUG
environment variable (see package runtime) togo
orcgo
, as in:export GODEBUG=netdns=go # force pure Go resolver export GODEBUG=netdns=cgo # force native resolver (cgo, win32)
Let’s create an example client resolver:
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run main.go <domain>")
return
}
domain := os.Args[1]
ips, err := net.LookupIP(domain)
if err != nil {
fmt.Printf("Could not get IPs: %v\n", err)
return
}
for _, ip := range ips {
fmt.Println(ip.String())
}
}
Build it with debug info:
$ go build -gcflags "all=-N -l" -o resolver ./main.go
Run:
$ ./resolver.go microsoft.com
2603:1020:201:10::10f
2603:1030:b:3::152
2603:1030:20e:3::23c
2603:1030:c02:8::14
2603:1010:3:3::5b
20.231.239.246
20.76.201.171
20.70.246.20
20.236.44.162
20.112.250.133
Run with strace
:
$ sudo strace -e openat ./resolver microsoft.com
…
[pid 48568] openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3
[pid 48568] openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
[pid 48568] openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 3
…
It only reads the basic system configuration files. It also does source address retrieval:
$ sudo strace -f -e trace=network ./resolver microsoft.com 2>&1
[pid 48674] socket(AF_INET6, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
[pid 48674] setsockopt(3, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0
[pid 48674] setsockopt(3, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0
[pid 48674] connect(3, {sa_family=AF_INET6, sin6_port=htons(9), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2603:1030:c02:8::14", &sin6_addr), sin6_scope_id=0}, 28) = 0
[pid 48674] getsockname(3, {sa_family=AF_INET6, sin6_port=htons(58438), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2001:db8:123:456:6af2:68fe:ff7c:e25c", &sin6_addr), sin6_scope_id=0}, [112 => 28]) = 0
[pid 48674] getpeername(3, {sa_family=AF_INET6, sin6_port=htons(9), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2603:1030:c02:8::14", &sin6_addr), sin6_scope_id=0}, [112 => 28]) = 0
Let make sure we don’t use libc
getaddrinfo()
. We use gdb
debugger for that:
$ gdb ./resolver
(gdb) set args microsoft.com
(gdb) break getaddrinfo
Breakpoint 1 at 0x11aa90
(gdb) run
2603:1010:3:3::5b
2603:1030:b:3::152
2603:1020:201:10::10f
2603:1030:20e:3::23c
2603:1030:c02:8::14
20.112.250.133
20.231.239.246
20.76.201.171
20.236.44.162
20.70.246.20
[Inferior 1 (process 48729) exited normally]
(gdb) Quit
The break point was not hit, so there were no getaddrinfo()
calls.
And if we recompile with cgo
resolver:
$ export GODEBUG=netdns=cgo
$ go build -gcflags "all=-N -l" -o resolver ./main.go
$ gdb ./resolver
(gdb) set args microsoft.com
(gdb) break getaddrinfo
Breakpoint 1 at 0x11aa90
(gdb) run
Thread 1 "resolver" hit Breakpoint 1, __GI_getaddrinfo (name=0x4000012120 "microsoft.com", service=0x0, hints=0x4000100540, pai=0x4000040048) at ./nss/getaddrinfo.c:2297
warning: 2297 ./nss/getaddrinfo.c: No such file or directory
The decision which resolver to use happens here and has a lot of logic to make a decision which stub resolver would be beneficial.
Go
supports the destination address sorting (as we see earlier with obtaining source adresses) in its go resolver but doesn’t provide a way to change the sorting rules (doesn’t read /etc/gai.conf
). The logic lives in https://github.com/golang/go/blob/master/src/net/addrselect.go.
8.2.2 Happy Eyeballs #
The standard golang library transparently supports RFC 6555 Happy Eyeballs: Success with Dual-Stack Hosts in net.Dialer
:
type Dialer struct {
…
// FallbackDelay specifies the length of time to wait before
// spawning a RFC 6555 Fast Fallback connection. That is, this
// is the amount of time to wait for IPv6 to succeed before
// assuming that IPv6 is misconfigured and falling back to
// IPv4.
//
// If zero, a default delay of 300ms is used.
// A negative value disables Fast Fallback support.
FallbackDelay time.Duration
…
}
Internally net.Dialer
in the DialContext()
method resolves hostname into adresses, splits them into two groups by address families, and runs the sysDialer.dialParallel()
function:
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
...
addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr)
if err != nil {
return nil, &OpError{Op: "dial", Net: network, Source: nil, Addr: nil, Err: err}
}
sd := &sysDialer{
Dialer: *d,
network: network,
address: address,
}
var primaries, fallbacks addrList
if d.dualStack() && network == "tcp" {
primaries, fallbacks = addrs.partition(isIPv4)
} else {
primaries = addrs
}
return sd.dialParallel(ctx, primaries, fallbacks)
}
where dialParallel()
issues an IPv6 connection request first, waits for FallbackDelay
or 300 ms by default in fallbackDelay()
.
// dialParallel races two copies of dialSerial, giving the first a
// head start. It returns the first established connection and
// closes the others. Otherwise it returns an error from the first
// primary address.
func (sd *sysDialer) dialParallel(ctx context.Context, primaries, fallbacks addrList) (Conn, error) {
if len(fallbacks) == 0 {
return sd.dialSerial(ctx, primaries)
}
returned := make(chan struct{})
defer close(returned)
type dialResult struct {
Conn
error
primary bool
done bool
}
results := make(chan dialResult) // unbuffered
startRacer := func(ctx context.Context, primary bool) {
ras := primaries
if !primary {
ras = fallbacks
}
c, err := sd.dialSerial(ctx, ras)
select {
case results <- dialResult{Conn: c, error: err, primary: primary, done: true}:
case <-returned:
if c != nil {
c.Close()
}
}
}
var primary, fallback dialResult
// Start the main racer.
primaryCtx, primaryCancel := context.WithCancel(ctx)
defer primaryCancel()
go startRacer(primaryCtx, true)
// Start the timer for the fallback racer.
fallbackTimer := time.NewTimer(sd.fallbackDelay())
defer fallbackTimer.Stop()
for {
select {
case <-fallbackTimer.C:
fallbackCtx, fallbackCancel := context.WithCancel(ctx)
defer fallbackCancel()
go startRacer(fallbackCtx, false)
case res := <-results:
if res.error == nil {
return res.Conn, nil
}
if res.primary {
primary = res
} else {
fallback = res
}
if primary.done && fallback.done {
return nil, primary.error
}
if res.primary && fallbackTimer.Stop() {
// If we were able to stop the timer, that means it
// was running (hadn't yet started the fallback), but
// we just got an error on the primary path, so start
// the fallback immediately (in 0 nanoseconds).
fallbackTimer.Reset(0)
}
}
}
}
func (d *Dialer) fallbackDelay() time.Duration {
if d.FallbackDelay > 0 {
return d.FallbackDelay
} else {
return 300 * time.Millisecond
}
}
8.3 Rust #
We are talking about Rust 1.80.0.
8.3.1 Resolver #
Rust has a ToSocketAddrs
trait:
pub trait ToSocketAddrs {
type Iter: Iterator<Item = SocketAddr>;
// Required method
fn to_socket_addrs(&self) -> Result<Self::Iter>;
}
where SocketAddr
:
pub enum SocketAddr {
V4(SocketAddrV4),
V6(SocketAddrV6),
}
The stdlib
comes with the trait implementation for the(&str, u16)
:
#[stable(feature = "rust1", since = "1.0.0")]
impl ToSocketAddrs for (&str, u16) {
type Iter = vec::IntoIter<SocketAddr>;
fn to_socket_addrs(&self) -> io::Result<vec::IntoIter<SocketAddr>> {
let (host, port) = *self;
// try to parse the host as a regular IP address first
if let Ok(addr) = host.parse::<Ipv4Addr>() {
let addr = SocketAddrV4::new(addr, port);
return Ok(vec![SocketAddr::V4(addr)].into_iter());
}
if let Ok(addr) = host.parse::<Ipv6Addr>() {
let addr = SocketAddrV6::new(addr, port, 0, 0);
return Ok(vec![SocketAddr::V6(addr)].into_iter());
}
resolve_socket_addr((host, port).try_into()?)
}
}
where (host, port).try_into()
uses the TryFrom
trait which is reciprocal for TryInto
:
impl<'a> TryFrom<(&'a str, u16)> for LookupHost {
type Error = io::Error;
fn try_from((host, port): (&'a str, u16)) -> io::Result<LookupHost> {
init();
run_with_cstr(host.as_bytes(), &|c_host| {
let mut hints: c::addrinfo = unsafe { mem::zeroed() };
hints.ai_socktype = c::SOCK_STREAM;
let mut res = ptr::null_mut();
unsafe {
cvt_gai(c::getaddrinfo(c_host.as_ptr(), ptr::null(), &hints, &mut res))
.map(|_| LookupHost { original: res, cur: res, port })
}
})
}
}
And here we can see here, it uses getaddrinfo()
from libc.
If you need a non-boking resolver to run it with async rust feature, you can, for example, use trust-dns-resolver
and tokio
:
Dependency:
[dependencies]
tokio = { version = "1", features = ["full"] }
trust-dns-resolver = "0.20"
Code:
use trust_dns_resolver::TokioAsyncResolver;
use trust_dns_resolver::config::*;
use trust_dns_resolver::system_conf::read_system_conf;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create the resolver from system configuration
let (resolver_config, mut resolver_opts) =read_system_conf()?;
// Specify the LookupIpStrategy you want
resolver_opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
// Create the resolver with the system configuration and modified options
let resolver = TokioAsyncResolver::tokio(resolver_config, resolver_opts)?;
let response = resolver.lookup_ip("microsoft.com").await?;
// Print the IP addresses
for ip in response.iter() {
println!("IP address: {}", ip);
}
Ok(())
}
It doesn’t follow the RFC 6724 Default Address Selection for Internet Protocol Version 6 (IPv6) but it has a lot of other interesting features.
Run it:
$ cargo build --release && ./target/release/resolver
Finished `release` profile [optimized] target(s) in 0.02s
IP address: 20.76.201.171
IP address: 20.236.44.162
IP address: 20.231.239.246
IP address: 20.70.246.20
IP address: 20.112.250.133
IP address: 2603:1010:3:3::5b
IP address: 2603:1030:c02:8::14
IP address: 2603:1030:b:3::152
IP address: 2603:1020:201:10::10f
IP address: 2603:1030:20e:3::23c
The strace()
output regarding reading system config files:
openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 9
openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 9
8.3.2 Happy Eyeballs #
Although there is no support in the standard library, several third-party libraries provide such capabilities.
8.4 Java and Netty #
We will be talking about OpenJDK
version 21.
8.4.1 Resolver #
The usual way to resolve a hostname is to run java.net.getAllByName(String host)
.
In order to change preferences of address families there are two system properties:
java.net.preferIPv4Stack
:
systemProperty
java.net.preferIPv4Stack
(default: false) If IPv6 is available on the operating system the underlying native socket will be, by default, an IPv6 socket which lets applications connect to, and accept connections from, both IPv4 and IPv6 hosts. However, in the case an application would rather use IPv4 only sockets, then this property can be set to true. The implication is that it will not be possible for the application to communicate with IPv6 only hosts.
java.net.preferIPv6Addresse
:
systemProperty
java.net.preferIPv6Addresses
(default: false) When dealing with a host which has both IPv4 and IPv6 addresses, and if IPv6 is available on the operating system, the default behavior is to prefer using IPv4 addresses over IPv6 ones. This is to ensure backward compatibility: for example, for applications that depend on the representation of an IPv4 address (e.g. 192.168.1.1). This property can be set to true to change that preference and use IPv6 addresses over IPv4 ones where possible, or system to preserve the order of the addresses as returned by the system-widejava.net.spi.InetAddressResolver
resolver.
Internally the following static variables are initialized in java.net.InetAdress
:
static {
// create the impl
impl = isIPv6Supported() ?
new Inet6AddressImpl() : new Inet4AddressImpl();
// impl must be initialized before calling this method
PLATFORM_LOOKUP_POLICY = initializePlatformLookupPolicy();
// create built-in resolver
BUILTIN_RESOLVER = createBuiltinInetAddressResolver();
}
which ends up in the native code and calls our old friend getaddrinfo()
:
JNIEXPORT jobjectArray JNICALL
Java_java_net_Inet6AddressImpl_lookupAllHostAddr(JNIEnv *env, jobject this,
jstring host,
jintcharacteristics) {
...
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = lookupCharacteristicsToAddressFamily(characteristics);
error = getaddrinfo(hostname, NULL, &hints, &res);
...
}
The lookupCharacteristicsToAddressFamily() function is controlling preferences from the above system properties:
int lookupCharacteristicsToAddressFamily(int characteristics) {
int ipv4 = characteristics & java_net_spi_InetAddressResolver_LookupPolicy_IPV4;
int ipv6 = characteristics & java_net_spi_InetAddressResolver_LookupPolicy_IPV6;
if (ipv4 != 0 && ipv6 == 0) {
return AF_INET;
}
if (ipv4 == 0 && ipv6 != 0) {
return AF_INET6;
}
return AF_UNSPEC;
}
We can make some conclusions now:
- By default preference is given to IPv4 addresses, they are returned first.
- You can reverse the logic by setting
preferIPv6Addresses
totrue
. - If you know that you are not going to use IPv6 at all, you can set
preferIPv4Stack
totrue
and the resolver stops querying for IPv6AAAA
records. - If you want to be dual stack ready and fully address agnostic with some drawback of the lack of Happy Eyeballs in Java, set
preferIPv6Addresses
tosystem
to delegate all responsibility togetaddrinfo()
.
However, networking services are typically written using the Netty asynchronous event-driven network framework. Netty provides its own asynchronous non-blocking resolver to work natively with its event loop-based model.
Let’s review its features in version 4.1.
The resolver’s documentation is here and to resolve an address you need to call resolveAll(String inetHost)
of SimpleNameResolver
.
For example:
package com.example;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFactory;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.resolver.ResolvedAddressTypes;
import io.netty.resolver.dns.DnsNameResolver;
import io.netty.resolver.dns.DnsNameResolverBuilder;
import io.netty.resolver.dns.DnsServerAddresses;
import io.netty.util.concurrent.Future;
import java.net.InetAddress;
import java.util.List;
public class App
{
public static void main( String[] args ) throws java.lang.InterruptedException
{
// Create an event loop group for handling DNS resolution
NioEventLoopGroup group = new NioEventLoopGroup();
try {
// Create a DnsNameResolver
DnsNameResolver resolver = new DnsNameResolverBuilder(group.next())
.channelFactory(new ChannelFactory<DatagramChannel>() {
@Override
public DatagramChannel newChannel() {
return new NioDatagramChannel();
}
})
.build();
// Resolve the domain name asynchronously
String domainName = "microsoft.com";
Future<List<InetAddress>> resolveFuture = resolver.resolveAll(domainName);
// Wait for the resolution to complete
List<InetAddress> inetAddresses = resolveFuture.sync().getNow();
// Print the resolved IP addresses
for (InetAddress inetAddress : inetAddresses) {
System.out.println("Resolved IP address: " + inetAddress);
}
} finally {
// Shut down the event loop group to release resources
group.shutdownGracefully();
}
}
}
The output:
Resolved IP address: microsoft.com/20.70.246.20
Resolved IP address: microsoft.com/20.231.239.246
Resolved IP address: microsoft.com/20.236.44.162
Resolved IP address: microsoft.com/20.112.250.133
Resolved IP address: microsoft.com/20.76.201.171
Resolved IP address: microsoft.com/2603:1010:3:3:0:0:0:5b
Resolved IP address: microsoft.com/2603:1030:20e:3:0:0:0:23c
Resolved IP address: microsoft.com/2603:1030:c02:8:0:0:0:14
Resolved IP address: microsoft.com/2603:1020:201:10:0:0:0:10f
Resolved IP address: microsoft.com/2603:1030:b:3:0:0:0:152
We can check that the resolver reads /etc/hosts
in resolveHostsFileEntries()
:
private List<InetAddress> resolveHostsFileEntries(String hostname) {
if (hostsFileEntriesResolver == null) {
return null;
}
List<InetAddress> addresses;
if (hostsFileEntriesResolver instanceof DefaultHostsFileEntriesResolver) {
addresses = ((DefaultHostsFileEntriesResolver) hostsFileEntriesResolver)
.addresses(hostname, resolvedAddressTypes);
} else {
InetAddress address = hostsFileEntriesResolver.address(hostname, resolvedAddressTypes);
addresses = address != null ? Collections.singletonList(address) : null;
}
return addresses == null && isLocalWindowsHost(hostname) ?
Collections.singletonList(LOCALHOST_ADDRESS) : addresses;
}
We can also specify the preference to resolve by settings DnsNameResolverBuilder()
:
DnsNameResolver resolver = new DnsNameResolverBuilder(group.next())
.channelFactory(new ChannelFactory<DatagramChannel>() {
@Override
public DatagramChannel newChannel() {
return new NioDatagramChannel();
}
}).resolvedAddressTypes(ResolvedAddressTypes.IPV6_PREFERRED)
.build();
Where ResolvedAddressTypes
:
/**
* Defined resolved address types.
*/
public enum ResolvedAddressTypes {
/**
* Only resolve IPv4 addresses
*/
IPV4_ONLY,
/**
* Only resolve IPv6 addresses
*/
IPV6_ONLY,
/**
* Prefer IPv4 addresses over IPv6 ones
*/
IPV4_PREFERRED,
/**
* Prefer IPv6 addresses over IPv4 ones
*/
IPV6_PREFERRED
}
Unfortunately, Netty does not support the system
options for IPv6 preference like the standard socket resolver does. The decision what to prefer happens in NetUtil:
private static final boolean IPV4_PREFERRED = SystemPropertyUtil.getBoolean("java.net.preferIPv4Stack", false);
...
static {
String prefer = SystemPropertyUtil.get("java.net.preferIPv6Addresses", "false");
if ("true".equalsIgnoreCase(prefer.trim())) {
IPV6_ADDRESSES_PREFERRED = true;
} else {
// Let's just use false in this case as only true is "forcing" ipv6.
IPV6_ADDRESSES_PREFERRED = false;
}
logger.debug("-Djava.net.preferIPv4Stack: {}", IPV4_PREFERRED);
logger.debug("-Djava.net.preferIPv6Addresses: {}", prefer);
...
}
If we set the properties:
-Djava.net.preferIPv6Addresses=true
And get the return:
Resolved IP address: microsoft.com/2603:1030:20e:3:0:0:0:23c
Resolved IP address: microsoft.com/2603:1030:b:3:0:0:0:152
Resolved IP address: microsoft.com/2603:1030:c02:8:0:0:0:14
Resolved IP address: microsoft.com/2603:1010:3:3:0:0:0:5b
Resolved IP address: microsoft.com/2603:1020:201:10:0:0:0:10f
Resolved IP address: microsoft.com/20.231.239.246
Resolved IP address: microsoft.com/20.236.44.162
Resolved IP address: microsoft.com/20.112.250.133
Resolved IP address: microsoft.com/20.70.246.20
Resolved IP address: microsoft.com/20.76.201.171
If we force to return only IPv4 with preferIPv4Stack
:
-Djava.net.preferIPv4Stack=true
Resolved IP address: microsoft.com/20.70.246.20
Resolved IP address: microsoft.com/20.231.239.246
Resolved IP address: microsoft.com/20.112.250.133
Resolved IP address: microsoft.com/20.76.201.171
Resolved IP address: microsoft.com/20.236.44.162
One more time there is no support of the system
argument for -Djava.net.preferIPv6Addresses=system
nor RFC 6724 with its section 6 Destination selection algorithm and Happy Eyeballs, so it’s completely on you to control you address preferences and the order in which they will appear in the result list from the resolver. This unfortunately could lead to a complexity with application configuration in the process of migration to IPv6 infrastructure.
The open issue https://github.com/netty/netty/issues/13400 has more info about the situation.
8.4.2 Java SecurityManager #
There is a well-known issue with Java regarding the caching of DNS resolutions.
If you have SecurityManager
enabled, the default caching TTL is infinite. To change it, edit the $JAVA_HOME/jre/lib/security/java.security
.
The defaults:
# The Java-level namelookup cache policy for successful lookups: # # any negative value: caching forever # any positive value: the number of seconds to cache an address for # zero: do not cache # # default value is forever (FOREVER). For security reasons, this # caching is made forever when a security manager is set. When a security # manager is not set, the default behavior in this implementation # is to cache for 30 seconds. # # NOTE: setting this to anything other than the default value can have # serious security implications. Do not set it unless # you are sure you are not exposed to DNS spoofing attack. # #networkaddress.cache.ttl=-1 # # The Java-level namelookup cache stale policy: # # any positive value: the number of seconds to use the stale names # zero: do not use stale names # negative values are ignored # # default value is 0 (NEVER). # #networkaddress.cache.stale.ttl=0 # The Java-level namelookup cache policy for failed lookups: # # any negative value: cache forever # any positive value: the number of seconds to cache negative lookup results # zero: do not cache # # In some Microsoft Windows networking environments that employ # the WINS name service in addition to DNS, name service lookups # that fail may take a noticeably long time to return (approx. 5 seconds). # For this reason the default caching policy is to maintain these # results for 10 seconds. # networkaddress.cache.negative.ttl=10
8.4.3 Happy eyeballs #
There is no support in standard libraries.
There is no support in 4.1 netty: https://github.com/netty/netty/issues/9540
The okhttp
(doesn’t use Netty internally and implemented its own event loop) supports it since the 5.0 version.
8.5 Node.js #
8.5.1 Resoler #
The libuv
is a C
library originally written for Node.js
to abstract non-blocking I/O operations.
It uses getaddrinfo()
internally.
libuv
provides asynchronous DNS resolution. For this it provides its owngetaddrinfo
replacement [3]. In the callback you can perform normal socket operations on the retrieved addresses. Let’s connect to Libera.chat to see an example of DNS resolution.[3] -
libuv
use the systemgetaddrinfo
in thelibuv
threadpool.
Code:
static void uv__getaddrinfo_work(struct uv__work* w) {
uv_getaddrinfo_t* req;
int err;
req = container_of(w, uv_getaddrinfo_t, work_req);
err = getaddrinfo(req->hostname, req->service, req->hints, &req->addrinfo);
req->retcode = uv__getaddrinfo_translate_error(err);
}
8.5.2 Happy Eyeballs #
There is ongoing discussion about adding a basic support in https://github.com/nodejs/node/issues/48145.
Read next chapter →