feat: clearly indicate which resolver we're using (#885)

See what we documented at https://github.com/ooni/spec/pull/257

Reference issue: https://github.com/ooni/probe/issues/2238

See also the related ooni/spec PR: https://github.com/ooni/spec/pull/257

See also https://github.com/ooni/probe/issues/2237

While there, bump webconnectivity@v0.5 version because this change
has an impact onto the generated data format.

The drop in coverage is unavoidable because we've written some
tests for `measurex` to ensure we deal with DNS resolvers and transport
names correctly depending on the splitting policy we use.

(However, `measurex` is only used for the `tor` experiment and, per
the step-by-step design document, new experiments should use
`measurexlite` instead, so this is hopefully fine(TM).)

While there, fix a broken integration test that does not run in `-short` mode.
This commit is contained in:
Simone Basso 2022-08-27 15:47:48 +02:00 committed by GitHub
parent c3964e43b3
commit 8a0c062844
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 362 additions and 59 deletions

View File

@ -29,7 +29,18 @@ func TestCreateAll(t *testing.T) {
exp := builder.NewExperiment()
good := (exp.Name() == name)
if !good {
t.Fatal("unexpected experiment name")
// We have introduced the concept of versioned experiments in
// https://github.com/ooni/probe-cli/pull/882. This works like
// in brew: we append @vX.Y to the experiment name. So, here
// we're stripping the version specification and retry.
index := strings.Index(name, "@")
if index >= 0 {
name = name[:index]
if good := (exp.Name() == name); good {
continue
}
}
t.Fatal("unexpected experiment name", exp.Name(), name)
}
}
}

View File

@ -36,7 +36,7 @@ func (m *Measurer) ExperimentName() string {
// ExperimentVersion implements model.ExperimentMeasurer.
func (m *Measurer) ExperimentVersion() string {
return "0.5.1"
return "0.5.2"
}
// Run implements model.ExperimentMeasurer.

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/tracex"
)
// WrapDNSXRoundTripper creates a new DNSXRoundTripper that
@ -42,7 +43,7 @@ func (txp *dnsxRoundTripperDB) RoundTrip(
response, err := txp.DNSTransport.RoundTrip(ctx, query)
finished := time.Since(txp.begin).Seconds()
txp.db.InsertIntoDNSRoundTrip(&DNSRoundTripEvent{
Network: txp.DNSTransport.Network(),
Network: tracex.ResolverNetworkAdaptNames(txp.DNSTransport.Network()),
Address: txp.DNSTransport.Address(),
Query: txp.maybeQueryBytes(query),
Started: started,

View File

@ -0,0 +1,47 @@
package measurex
import (
"context"
"testing"
"github.com/miekg/dns"
"github.com/ooni/probe-cli/v3/internal/model/mocks"
"github.com/ooni/probe-cli/v3/internal/netxlite"
)
func TestDNSXModifiesStdlibTransportName(t *testing.T) {
// See https://github.com/ooni/spec/pull/257 for more information.
child := netxlite.NewDNSOverGetaddrinfoTransport()
mx := NewMeasurerWithDefaultSettings()
dbout := &MeasurementDB{}
txp := mx.WrapDNSXRoundTripper(dbout, child)
ctx, cancel := context.WithCancel(context.Background())
cancel() // we want to fail immediately
query := &mocks.DNSQuery{
MockDomain: func() string {
return "dns.google"
},
MockType: func() uint16 {
return dns.TypeANY
},
MockBytes: func() ([]byte, error) {
return []byte{}, nil
},
MockID: func() uint16 {
return 1453
},
}
_, _ = txp.RoundTrip(ctx, query)
measurement := dbout.AsMeasurement()
var good int
for _, rtinfo := range measurement.DNSRoundTrip {
network := rtinfo.Network
if network != netxlite.StdlibResolverSystem {
t.Fatal("unexpected network", network)
}
good++
}
if good < 1 {
t.Fatal("no good entry seen")
}
}

View File

@ -13,6 +13,7 @@ import (
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/tracex"
)
// WrapResolver creates a new Resolver that saves events into the WritableDB.
@ -105,7 +106,7 @@ func (r *resolverDB) LookupHost(ctx context.Context, domain string) ([]string, e
func (r *resolverDB) saveLookupResults(domain string, started, finished float64,
err error, addrs []string, qtype string) {
ev := &DNSLookupEvent{
Network: r.Resolver.Network(),
Network: tracex.ResolverNetworkAdaptNames(r.Resolver.Network()),
Address: r.Resolver.Address(),
Failure: NewFailure(err),
Domain: domain,
@ -158,7 +159,7 @@ func (r *resolverDB) LookupHTTPS(ctx context.Context, domain string) (*model.HTT
https, err := r.Resolver.LookupHTTPS(ctx, domain)
finished := time.Since(r.begin).Seconds()
ev := &DNSLookupEvent{
Network: r.Resolver.Network(),
Network: tracex.ResolverNetworkAdaptNames(r.Resolver.Network()),
Address: r.Resolver.Address(),
Domain: domain,
QueryType: "HTTPS",

View File

@ -0,0 +1,58 @@
package measurex
import (
"context"
"testing"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
)
func TestResolverModifiesStdlibResolverName(t *testing.T) {
// See https://github.com/ooni/spec/pull/257 for more information.
t.Run("for LookupHost", func(t *testing.T) {
child := netxlite.NewStdlibResolver(model.DiscardLogger)
mx := NewMeasurerWithDefaultSettings()
dbout := &MeasurementDB{}
txp := mx.WrapResolver(dbout, child)
ctx, cancel := context.WithCancel(context.Background())
cancel() // we want to fail immediately
_, _ = txp.LookupHost(ctx, "dns.google")
measurement := dbout.AsMeasurement()
var good int
for _, rtinfo := range measurement.LookupHost {
network := rtinfo.Network
if network != netxlite.StdlibResolverSystem {
t.Fatal("unexpected network", network)
}
good++
}
if good < 1 {
t.Fatal("no good entry seen")
}
})
t.Run("for LookupHTTPS", func(t *testing.T) {
child := netxlite.NewStdlibResolver(model.DiscardLogger)
mx := NewMeasurerWithDefaultSettings()
dbout := &MeasurementDB{}
txp := mx.WrapResolver(dbout, child)
ctx, cancel := context.WithCancel(context.Background())
cancel() // we want to fail immediately
_, _ = txp.LookupHTTPS(ctx, "dns.google")
measurement := dbout.AsMeasurement()
var good int
for _, rtinfo := range measurement.LookupHTTPSSvc {
network := rtinfo.Network
if network != netxlite.StdlibResolverSystem {
t.Fatal("unexpected network", network)
}
good++
}
if good < 1 {
t.Fatal("no good entry seen")
}
})
}

View File

@ -301,8 +301,12 @@ func TestNewWrappedResolvers(t *testing.T) {
if resolvert.tx != trace {
t.Fatal("invalid trace")
}
if resolver.Network() != "system" {
t.Fatal("unexpected resolver network")
switch network := resolver.Network(); network {
case netxlite.StdlibResolverGetaddrinfo,
netxlite.StdlibResolverGolangNetResolver:
// ok
default:
t.Fatal("unexpected resolver network", network)
}
})
}

View File

@ -24,6 +24,11 @@ type dnsOverGetaddrinfoTransport struct {
testableLookupANY func(ctx context.Context, domain string) ([]string, string, error)
}
// NewDNSOverGetaddrinfoTransport creates a new dns-over-getaddrinfo transport.
func NewDNSOverGetaddrinfoTransport() model.DNSTransport {
return &dnsOverGetaddrinfoTransport{}
}
var _ model.DNSTransport = &dnsOverGetaddrinfoTransport{}
func (txp *dnsOverGetaddrinfoTransport) RoundTrip(

View File

@ -47,7 +47,7 @@
//
// 1. establishing a TCP connection;
//
// 2. performing a domain name resolution with the "system" resolver
// 2. performing a domain name resolution with the "stdlib" resolver
// (i.e., getaddrinfo on Unix) or custom DNS transports (e.g., DoT, DoH);
//
// 3. performing the TLS handshake;

View File

@ -2,6 +2,23 @@ package netxlite
import "errors"
// Name of the resolver we use when we link with libc and use getaddrinfo directly.
//
// See https://github.com/ooni/spec/pull/257 for more info.
const StdlibResolverGetaddrinfo = "getaddrinfo"
// Name of the resolver we use when we don't link with libc and use net.Resolver.
//
// See https://github.com/ooni/spec/pull/257 for more info.
const StdlibResolverGolangNetResolver = "golang_net_resolver"
// Legacy name of the resolver we use when we're don't know whether we're using
// getaddrinfo, but we're using net.Resolver, and we're splitting the answer
// in two A and AAAA queries. Eventually will become deprecated.
//
// See https://github.com/ooni/spec/pull/257 for more info.
const StdlibResolverSystem = "system"
// ErrGetaddrinfo represents a getaddrinfo failure.
type ErrGetaddrinfo struct {
// Err is the error proper.

View File

@ -28,10 +28,13 @@ import (
// been used to implement the getaddrinfo resolver.
//
// This is the CGO_ENABLED=1 implementation of this function, which
// always returns the string "system", because in this scenario
// always returns the string [StdlibResolverGetaddrinfo], because in this scenario
// we are actually calling the getaddrinfo libc function.
//
// See https://github.com/ooni/spec/pull/257 for more information on how
// we evolved our naming of the "stdlib" resolver over time.
func getaddrinfoResolverNetwork() string {
return "system"
return StdlibResolverGetaddrinfo
}
// getaddrinfoLookupANY attempts to perform an ANY lookup using getaddrinfo.

View File

@ -11,13 +11,16 @@ import (
// been used to implement the getaddrinfo resolver.
//
// This is the CGO_ENABLED=0 implementation of this function, which
// always returns the string "go", because in this scenario we are actually
// using whatever resolver is used under the hood by the stdlib.
// always returns the string [StdlibResolverGolangNetResolver], because in this scenario
// we are actually using whatever resolver is used under the hood by the stdlib.
//
// See https://github.com/ooni/probe/issues/2029#issuecomment-1140805266
// for an explanation of why calling this resolver "netgo" is wrong.
// for an explanation of why calling this resolver "netgo" was wrong.
//
// See https://github.com/ooni/spec/pull/257 for additional documentation
// regarding using "golang_net_resolver" instead of "go".
func getaddrinfoResolverNetwork() string {
return "go"
return StdlibResolverGolangNetResolver
}
// getaddrinfoLookupANY attempts to perform an ANY lookup using getaddrinfo.

View File

@ -20,11 +20,11 @@ import (
// ErrNoDNSTransport is the error returned when you attempt to perform
// a DNS operation that requires a custom DNSTransport (e.g., DNSOverHTTPSTransport)
// but you are using the "system" resolver instead.
// but you are using the "stdlib" resolver instead.
var ErrNoDNSTransport = errors.New("operation requires a DNS transport")
// NewStdlibResolver creates a new Resolver by combining WrapResolver
// with an internal "system" resolver type. The list of optional wrappers
// with an internal "stdlib" resolver type. The list of optional wrappers
// allow to wrap the underlying getaddrinfo transport. Any nil wrapper
// will be silently ignored by the code that performs the wrapping.
func NewStdlibResolver(logger model.DebugLogger, wrappers ...model.DNSTransportWrapper) model.Resolver {
@ -45,7 +45,7 @@ func NewParallelDNSOverHTTPSResolver(logger model.DebugLogger, URL string) model
// implies, this function returns an unwrapped resolver.
func NewUnwrappedStdlibResolver(wrappers ...model.DNSTransportWrapper) model.Resolver {
return &resolverSystem{
t: WrapDNSTransport(&dnsOverGetaddrinfoTransport{}, wrappers...),
t: WrapDNSTransport(NewDNSOverGetaddrinfoTransport(), wrappers...),
}
}

View File

@ -347,7 +347,7 @@ func TestResolverLogger(t *testing.T) {
return expected, nil
},
MockNetwork: func() string {
return "system"
return StdlibResolverGetaddrinfo
},
MockAddress: func() string {
return ""
@ -381,7 +381,7 @@ func TestResolverLogger(t *testing.T) {
return nil, expected
},
MockNetwork: func() string {
return "system"
return StdlibResolverGetaddrinfo
},
MockAddress: func() string {
return ""
@ -420,7 +420,7 @@ func TestResolverLogger(t *testing.T) {
return expected, nil
},
MockNetwork: func() string {
return "system"
return StdlibResolverGetaddrinfo
},
MockAddress: func() string {
return ""
@ -454,7 +454,7 @@ func TestResolverLogger(t *testing.T) {
return nil, expected
},
MockNetwork: func() string {
return "system"
return StdlibResolverGetaddrinfo
},
MockAddress: func() string {
return ""
@ -509,7 +509,7 @@ func TestResolverLogger(t *testing.T) {
return expected, nil
},
MockNetwork: func() string {
return "system"
return StdlibResolverGetaddrinfo
},
MockAddress: func() string {
return ""
@ -543,7 +543,7 @@ func TestResolverLogger(t *testing.T) {
return nil, expected
},
MockNetwork: func() string {
return "system"
return StdlibResolverGetaddrinfo
},
MockAddress: func() string {
return ""

View File

@ -10,6 +10,7 @@ import (
"time"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
)
// ResolverSaver is a resolver that saves events.
@ -42,7 +43,7 @@ func (r *ResolverSaver) LookupHost(ctx context.Context, hostname string) ([]stri
r.Saver.Write(&EventResolveStart{&EventValue{
Address: r.Resolver.Address(),
Hostname: hostname,
Proto: r.Resolver.Network(),
Proto: r.Network(),
Time: start,
}})
addrs, err := r.Resolver.LookupHost(ctx, hostname)
@ -53,14 +54,29 @@ func (r *ResolverSaver) LookupHost(ctx context.Context, hostname string) ([]stri
Duration: stop.Sub(start),
Err: NewFailureStr(err),
Hostname: hostname,
Proto: r.Resolver.Network(),
Proto: r.Network(),
Time: stop,
}})
return addrs, err
}
// ResolverNetworkAdaptNames makes sure we map the [netxlite.StdlibResolverGolangNetResolver] and
// [netxlite.StdlibResolverGetaddrinfo] resolver names to [netxlite.StdlibResolverSystem]. You MUST
// call this function when your resolver splits the "stdlib" resolver results into two fake AAAA
// and A queries rather than faking a single ANY query.
//
// See https://github.com/ooni/spec/pull/257 for more information.
func ResolverNetworkAdaptNames(input string) string {
switch input {
case netxlite.StdlibResolverGetaddrinfo, netxlite.StdlibResolverGolangNetResolver:
return netxlite.StdlibResolverSystem
default:
return input
}
}
func (r *ResolverSaver) Network() string {
return r.Resolver.Network()
return ResolverNetworkAdaptNames(r.Resolver.Network())
}
func (r *ResolverSaver) Address() string {
@ -112,7 +128,7 @@ func (txp *DNSTransportSaver) RoundTrip(
txp.Saver.Write(&EventDNSRoundTripStart{&EventValue{
Address: txp.DNSTransport.Address(),
DNSQuery: dnsMaybeQueryBytes(query),
Proto: txp.DNSTransport.Network(),
Proto: txp.Network(),
Time: start,
}})
response, err := txp.DNSTransport.RoundTrip(ctx, query)
@ -123,14 +139,14 @@ func (txp *DNSTransportSaver) RoundTrip(
DNSResponse: dnsMaybeResponseBytes(response),
Duration: stop.Sub(start),
Err: NewFailureStr(err),
Proto: txp.DNSTransport.Network(),
Proto: txp.Network(),
Time: stop,
}})
return response, err
}
func (txp *DNSTransportSaver) Network() string {
return txp.DNSTransport.Network()
return ResolverNetworkAdaptNames(txp.DNSTransport.Network())
}
func (txp *DNSTransportSaver) Address() string {

View File

@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/miekg/dns"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/model/mocks"
"github.com/ooni/probe-cli/v3/internal/netxlite"
@ -75,7 +76,7 @@ func TestResolverSaver(t *testing.T) {
reso := saver.WrapResolver(newFakeResolverWithResult(expected))
addrs, err := reso.LookupHost(context.Background(), "www.google.com")
if err != nil {
t.Fatal("expected nil error here")
t.Fatal(err)
}
if !reflect.DeepEqual(addrs, expected) {
t.Fatal("not the result we expected")
@ -112,19 +113,56 @@ func TestResolverSaver(t *testing.T) {
t.Fatal("the saved time is wrong")
}
})
t.Run("with stdlib resolver there's correct .Network remapping", func(t *testing.T) {
saver := &Saver{}
reso := saver.WrapResolver(netxlite.NewStdlibResolver(model.DiscardLogger))
ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately fail the operation
_, _ = reso.LookupHost(ctx, "www.google.com")
// basically, we just want to ensure that the engine name is converted
ev := saver.Read()
if len(ev) != 2 {
t.Fatal("expected number of events")
}
if ev[0].Value().Proto != netxlite.StdlibResolverSystem {
t.Fatal("unexpected Proto")
}
if ev[1].Value().Proto != netxlite.StdlibResolverSystem {
t.Fatal("unexpected Proto")
}
})
})
t.Run("Network", func(t *testing.T) {
saver := &Saver{}
child := &mocks.Resolver{
MockNetwork: func() string {
return "x"
},
}
reso := saver.WrapResolver(child)
if reso.Network() != "x" {
t.Fatal("unexpected result")
}
t.Run("when using a custom resolver", func(t *testing.T) {
saver := &Saver{}
child := &mocks.Resolver{
MockNetwork: func() string {
return "x"
},
}
reso := saver.WrapResolver(child)
if reso.Network() != "x" {
t.Fatal("unexpected result")
}
})
t.Run("when using the stdlib resolver", func(t *testing.T) {
child := netxlite.NewStdlibResolver(model.DiscardLogger)
switch network := child.Network(); network {
case netxlite.StdlibResolverGetaddrinfo,
netxlite.StdlibResolverGolangNetResolver:
// ok
default:
t.Fatal("unexpected child resolver network", network)
}
saver := &Saver{}
reso := saver.WrapResolver(child)
if network := reso.Network(); network != netxlite.StdlibResolverSystem {
t.Fatal("unexpected wrapped resolver network", network)
}
})
})
t.Run("Address", func(t *testing.T) {
@ -326,19 +364,70 @@ func TestDNSTransportSaver(t *testing.T) {
t.Fatal("the saved time is wrong")
}
})
t.Run("with getaddrinfo transport there's correct .Network remapping", func(t *testing.T) {
saver := &Saver{}
reso := saver.WrapDNSTransport(netxlite.NewDNSOverGetaddrinfoTransport())
ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately fail the operation
query := &mocks.DNSQuery{
MockBytes: func() ([]byte, error) {
return []byte{}, nil
},
MockType: func() uint16 {
return dns.TypeANY
},
MockID: func() uint16 {
return 1453
},
MockDomain: func() string {
return "dns.google"
},
}
_, _ = reso.RoundTrip(ctx, query)
// basically, we just want to ensure that the engine name is converted
ev := saver.Read()
if len(ev) != 2 {
t.Fatal("expected number of events")
}
if ev[0].Value().Proto != netxlite.StdlibResolverSystem {
t.Fatal("unexpected Proto")
}
if ev[1].Value().Proto != netxlite.StdlibResolverSystem {
t.Fatal("unexpected Proto")
}
})
})
t.Run("Network", func(t *testing.T) {
saver := &Saver{}
child := &mocks.DNSTransport{
MockNetwork: func() string {
return "x"
},
}
txp := saver.WrapDNSTransport(child)
if txp.Network() != "x" {
t.Fatal("unexpected result")
}
t.Run("with custom child transport", func(t *testing.T) {
saver := &Saver{}
child := &mocks.DNSTransport{
MockNetwork: func() string {
return "x"
},
}
txp := saver.WrapDNSTransport(child)
if txp.Network() != "x" {
t.Fatal("unexpected result")
}
})
t.Run("when using the stdlib resolver", func(t *testing.T) {
child := netxlite.NewDNSOverGetaddrinfoTransport()
switch network := child.Network(); network {
case netxlite.StdlibResolverGetaddrinfo,
netxlite.StdlibResolverGolangNetResolver:
// ok
default:
t.Fatal("unexpected child resolver network", network)
}
saver := &Saver{}
reso := saver.WrapDNSTransport(child)
if network := reso.Network(); network != netxlite.StdlibResolverSystem {
t.Fatal("unexpected wrapped resolver network", network)
}
})
})
t.Run("Address", func(t *testing.T) {
@ -429,3 +518,45 @@ func newFakeResolverWithResult(r []string) model.Resolver {
},
}
}
func TestResolverNetworkAdaptNames(t *testing.T) {
type args struct {
input string
}
tests := []struct {
name string
args args
want string
}{{
name: "with StdlibResolverGetaddrinfo",
args: args{
input: netxlite.StdlibResolverGetaddrinfo,
},
want: netxlite.StdlibResolverSystem,
}, {
name: "with StdlibResolverGolangNetResolver",
args: args{
input: netxlite.StdlibResolverGolangNetResolver,
},
want: netxlite.StdlibResolverSystem,
}, {
name: "with StdlibResolverSystem",
args: args{
input: netxlite.StdlibResolverSystem,
},
want: netxlite.StdlibResolverSystem,
}, {
name: "with any other name",
args: args{
input: "doh",
},
want: "doh",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ResolverNetworkAdaptNames(tt.args.input); got != tt.want {
t.Errorf("ResolverNetworkAdaptNames() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -25,7 +25,7 @@ Index:
- [chapter04](chapter04) shows how to establish QUIC sessions;
- [chapter05](chapter05) is about the "system" DNS resolver;
- [chapter05](chapter05) is about the "stdlib" DNS resolver;
- [chapter06](chapter06) discusses custom DNS-over-UDP resolvers;

View File

@ -1,10 +1,13 @@
# Chapter I: Using the "system" DNS resolver
# Chapter I: Using the "stdlib" DNS resolver
In this chapter we will write together a `main.go` file that
uses the "system" DNS resolver to lookup domain names.
uses the "stdlib" DNS resolver to lookup domain names.
The "system" resolver is the one used by `getaddrinfo` on Unix.
The "stdlib" resolver is `getaddrinfo` on Unix. If we're compiled with
`CGO_ENABLED=1`, we use the `getaddrinfo` stdlib call directly. Otherwise,
we use the `net.Resolver` resolver, which may or may not use
`getaddrinfo` (or equivalent stdlib calls) under the hood.
(This file is auto-generated from the corresponding source file,
so make sure you don't edit it manually.)
@ -114,4 +117,4 @@ should cause a timeout error, because the timeout is ridicolously small.
## Conclusions
We have seen how to use the "system" DNS resolver.
We have seen how to use the "stdlib" DNS resolver.

View File

@ -1,11 +1,14 @@
// -=-=- StartHere -=-=-
//
// # Chapter I: Using the "system" DNS resolver
// # Chapter I: Using the "stdlib" DNS resolver
//
// In this chapter we will write together a `main.go` file that
// uses the "system" DNS resolver to lookup domain names.
// uses the "stdlib" DNS resolver to lookup domain names.
//
// The "system" resolver is the one used by `getaddrinfo` on Unix.
// The "stdlib" resolver is `getaddrinfo` on Unix. If we're compiled with
// `CGO_ENABLED=1`, we use the `getaddrinfo` stdlib call directly. Otherwise,
// we use the `net.Resolver` resolver, which may or may not use
// `getaddrinfo` (or equivalent stdlib calls) under the hood.
//
// (This file is auto-generated from the corresponding source file,
// so make sure you don't edit it manually.)
@ -115,4 +118,4 @@ func fatal(err error) {
//
// ## Conclusions
//
// We have seen how to use the "system" DNS resolver.
// We have seen how to use the "stdlib" DNS resolver.