8a0c062844
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.
430 lines
12 KiB
Go
430 lines
12 KiB
Go
package netxlite
|
|
|
|
//
|
|
// DNS resolver
|
|
//
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/miekg/dns"
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
|
"golang.org/x/net/idna"
|
|
)
|
|
|
|
// 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 "stdlib" resolver instead.
|
|
var ErrNoDNSTransport = errors.New("operation requires a DNS transport")
|
|
|
|
// NewStdlibResolver creates a new Resolver by combining WrapResolver
|
|
// 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 {
|
|
return WrapResolver(logger, NewUnwrappedStdlibResolver(wrappers...))
|
|
}
|
|
|
|
// NewParallelDNSOverHTTPSResolver creates a new DNS over HTTPS resolver
|
|
// that uses the standard library for all operations. This function constructs
|
|
// all the building blocks and calls WrapResolver on the returned resolver.
|
|
func NewParallelDNSOverHTTPSResolver(logger model.DebugLogger, URL string) model.Resolver {
|
|
client := &http.Client{Transport: NewHTTPTransportStdlib(logger)}
|
|
txp := WrapDNSTransport(NewUnwrappedDNSOverHTTPSTransport(client, URL))
|
|
return WrapResolver(logger, NewUnwrappedParallelResolver(txp))
|
|
}
|
|
|
|
// NewUnwrappedStdlibResolver returns a new, unwrapped resolver using the standard
|
|
// library (i.e., getaddrinfo if possible and &net.Resolver{} otherwise). As the name
|
|
// implies, this function returns an unwrapped resolver.
|
|
func NewUnwrappedStdlibResolver(wrappers ...model.DNSTransportWrapper) model.Resolver {
|
|
return &resolverSystem{
|
|
t: WrapDNSTransport(NewDNSOverGetaddrinfoTransport(), wrappers...),
|
|
}
|
|
}
|
|
|
|
// NewSerialUDPResolver creates a new Resolver using DNS-over-UDP
|
|
// that performs serial A/AAAA lookups during LookupHost.
|
|
//
|
|
// Deprecated: use NewParallelResolverUDP.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - logger is the logger to use
|
|
//
|
|
// - dialer is the dialer to create and connect UDP conns
|
|
//
|
|
// - address is the server address (e.g., 1.1.1.1:53)
|
|
//
|
|
// - wrappers is the optional list of wrappers to wrap the underlying
|
|
// transport. Any nil wrapper will be silently ignored.
|
|
func NewSerialUDPResolver(logger model.DebugLogger, dialer model.Dialer,
|
|
address string, wrappers ...model.DNSTransportWrapper) model.Resolver {
|
|
return WrapResolver(logger, NewUnwrappedSerialResolver(
|
|
WrapDNSTransport(NewUnwrappedDNSOverUDPTransport(dialer, address), wrappers...),
|
|
))
|
|
}
|
|
|
|
// NewParallelUDPResolver creates a new Resolver using DNS-over-UDP
|
|
// that performs parallel A/AAAA lookups during LookupHost.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - logger is the logger to use
|
|
//
|
|
// - dialer is the dialer to create and connect UDP conns
|
|
//
|
|
// - address is the server address (e.g., 1.1.1.1:53)
|
|
//
|
|
// - wrappers is the optional list of wrappers to wrap the underlying
|
|
// transport. Any nil wrapper will be silently ignored.
|
|
func NewParallelUDPResolver(logger model.DebugLogger, dialer model.Dialer,
|
|
address string, wrappers ...model.DNSTransportWrapper) model.Resolver {
|
|
return WrapResolver(logger, NewUnwrappedParallelResolver(
|
|
WrapDNSTransport(NewUnwrappedDNSOverUDPTransport(dialer, address), wrappers...),
|
|
))
|
|
}
|
|
|
|
// WrapResolver creates a new resolver that wraps an
|
|
// existing resolver to add these properties:
|
|
//
|
|
// 1. handles IDNA;
|
|
//
|
|
// 2. performs logging;
|
|
//
|
|
// 3. short-circuits IP addresses like getaddrinfo does (i.e.,
|
|
// resolving "1.1.1.1" yields []string{"1.1.1.1"};
|
|
//
|
|
// 4. wraps errors;
|
|
//
|
|
// 5. enforces reasonable timeouts (
|
|
// see https://github.com/ooni/probe/issues/1726).
|
|
//
|
|
// This is a low-level factory. Use only if out of alternatives.
|
|
func WrapResolver(logger model.DebugLogger, resolver model.Resolver) model.Resolver {
|
|
return &resolverIDNA{
|
|
Resolver: &resolverLogger{
|
|
Resolver: &resolverShortCircuitIPAddr{
|
|
Resolver: &resolverErrWrapper{
|
|
Resolver: resolver,
|
|
},
|
|
},
|
|
Logger: logger,
|
|
},
|
|
}
|
|
}
|
|
|
|
// resolverSystem is the system resolver.
|
|
type resolverSystem struct {
|
|
t model.DNSTransport
|
|
}
|
|
|
|
var _ model.Resolver = &resolverSystem{}
|
|
|
|
func (r *resolverSystem) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
|
encoder := &DNSEncoderMiekg{}
|
|
query := encoder.Encode(hostname, dns.TypeANY, false)
|
|
trace := ContextTraceOrDefault(ctx)
|
|
start := trace.TimeNow()
|
|
resp, err := r.t.RoundTrip(ctx, query)
|
|
end := trace.TimeNow()
|
|
if err != nil {
|
|
trace.OnDNSRoundTripForLookupHost(start, r, query, resp, []string{}, err, end)
|
|
return []string{}, err
|
|
}
|
|
addrs, err := resp.DecodeLookupHost()
|
|
trace.OnDNSRoundTripForLookupHost(start, r, query, resp, addrs, err, end)
|
|
return addrs, err
|
|
}
|
|
|
|
func (r *resolverSystem) Network() string {
|
|
return r.t.Network()
|
|
}
|
|
|
|
func (r *resolverSystem) Address() string {
|
|
return r.t.Address()
|
|
}
|
|
|
|
func (r *resolverSystem) CloseIdleConnections() {
|
|
r.t.CloseIdleConnections()
|
|
}
|
|
|
|
func (r *resolverSystem) LookupHTTPS(
|
|
ctx context.Context, domain string) (*model.HTTPSSvc, error) {
|
|
return nil, ErrNoDNSTransport
|
|
}
|
|
|
|
func (r *resolverSystem) LookupNS(
|
|
ctx context.Context, domain string) ([]*net.NS, error) {
|
|
return nil, ErrNoDNSTransport
|
|
}
|
|
|
|
// resolverLogger is a resolver that emits events
|
|
type resolverLogger struct {
|
|
Resolver model.Resolver
|
|
Logger model.DebugLogger
|
|
}
|
|
|
|
var _ model.Resolver = &resolverLogger{}
|
|
|
|
func (r *resolverLogger) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
|
prefix := fmt.Sprintf("resolve[A,AAAA] %s with %s (%s)", hostname, r.Network(), r.Address())
|
|
r.Logger.Debugf("%s...", prefix)
|
|
start := time.Now()
|
|
addrs, err := r.Resolver.LookupHost(ctx, hostname)
|
|
elapsed := time.Since(start)
|
|
if err != nil {
|
|
r.Logger.Debugf("%s... %s in %s", prefix, err, elapsed)
|
|
return nil, err
|
|
}
|
|
r.Logger.Debugf("%s... %+v in %s", prefix, addrs, elapsed)
|
|
return addrs, nil
|
|
}
|
|
|
|
func (r *resolverLogger) LookupHTTPS(
|
|
ctx context.Context, domain string) (*model.HTTPSSvc, error) {
|
|
prefix := fmt.Sprintf("resolve[HTTPS] %s with %s (%s)", domain, r.Network(), r.Address())
|
|
r.Logger.Debugf("%s...", prefix)
|
|
start := time.Now()
|
|
https, err := r.Resolver.LookupHTTPS(ctx, domain)
|
|
elapsed := time.Since(start)
|
|
if err != nil {
|
|
r.Logger.Debugf("%s... %s in %s", prefix, err, elapsed)
|
|
return nil, err
|
|
}
|
|
alpn := https.ALPN
|
|
a := https.IPv4
|
|
aaaa := https.IPv6
|
|
r.Logger.Debugf("%s... %+v %+v %+v in %s", prefix, alpn, a, aaaa, elapsed)
|
|
return https, nil
|
|
}
|
|
|
|
func (r *resolverLogger) Address() string {
|
|
return r.Resolver.Address()
|
|
}
|
|
|
|
func (r *resolverLogger) Network() string {
|
|
return r.Resolver.Network()
|
|
}
|
|
|
|
func (r *resolverLogger) CloseIdleConnections() {
|
|
r.Resolver.CloseIdleConnections()
|
|
}
|
|
|
|
func (r *resolverLogger) LookupNS(
|
|
ctx context.Context, domain string) ([]*net.NS, error) {
|
|
prefix := fmt.Sprintf("resolve[NS] %s with %s (%s)", domain, r.Network(), r.Address())
|
|
r.Logger.Debugf("%s...", prefix)
|
|
start := time.Now()
|
|
ns, err := r.Resolver.LookupNS(ctx, domain)
|
|
elapsed := time.Since(start)
|
|
if err != nil {
|
|
r.Logger.Debugf("%s... %s in %s", prefix, err, elapsed)
|
|
return nil, err
|
|
}
|
|
r.Logger.Debugf("%s... %+v in %s", prefix, ns, elapsed)
|
|
return ns, nil
|
|
}
|
|
|
|
// resolverIDNA supports resolving Internationalized Domain Names.
|
|
//
|
|
// See RFC3492 for more information.
|
|
type resolverIDNA struct {
|
|
Resolver model.Resolver
|
|
}
|
|
|
|
var _ model.Resolver = &resolverIDNA{}
|
|
|
|
func (r *resolverIDNA) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
|
host, err := idna.ToASCII(hostname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r.Resolver.LookupHost(ctx, host)
|
|
}
|
|
|
|
func (r *resolverIDNA) LookupHTTPS(
|
|
ctx context.Context, domain string) (*model.HTTPSSvc, error) {
|
|
host, err := idna.ToASCII(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r.Resolver.LookupHTTPS(ctx, host)
|
|
}
|
|
|
|
func (r *resolverIDNA) Network() string {
|
|
return r.Resolver.Network()
|
|
}
|
|
|
|
func (r *resolverIDNA) Address() string {
|
|
return r.Resolver.Address()
|
|
}
|
|
|
|
func (r *resolverIDNA) CloseIdleConnections() {
|
|
r.Resolver.CloseIdleConnections()
|
|
}
|
|
|
|
func (r *resolverIDNA) LookupNS(
|
|
ctx context.Context, domain string) ([]*net.NS, error) {
|
|
host, err := idna.ToASCII(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r.Resolver.LookupNS(ctx, host)
|
|
}
|
|
|
|
// resolverShortCircuitIPAddr recognizes when the input hostname is an
|
|
// IP address and returns it immediately to the caller.
|
|
type resolverShortCircuitIPAddr struct {
|
|
Resolver model.Resolver
|
|
}
|
|
|
|
var _ model.Resolver = &resolverShortCircuitIPAddr{}
|
|
|
|
func (r *resolverShortCircuitIPAddr) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
|
if net.ParseIP(hostname) != nil {
|
|
return []string{hostname}, nil
|
|
}
|
|
return r.Resolver.LookupHost(ctx, hostname)
|
|
}
|
|
|
|
func (r *resolverShortCircuitIPAddr) LookupHTTPS(ctx context.Context, hostname string) (*model.HTTPSSvc, error) {
|
|
if net.ParseIP(hostname) != nil {
|
|
https := &model.HTTPSSvc{}
|
|
if isIPv6(hostname) {
|
|
https.IPv6 = append(https.IPv6, hostname)
|
|
} else {
|
|
https.IPv4 = append(https.IPv4, hostname)
|
|
}
|
|
return https, nil
|
|
}
|
|
return r.Resolver.LookupHTTPS(ctx, hostname)
|
|
}
|
|
|
|
func (r *resolverShortCircuitIPAddr) Network() string {
|
|
return r.Resolver.Network()
|
|
}
|
|
|
|
func (r *resolverShortCircuitIPAddr) Address() string {
|
|
return r.Resolver.Address()
|
|
}
|
|
|
|
func (r *resolverShortCircuitIPAddr) CloseIdleConnections() {
|
|
r.Resolver.CloseIdleConnections()
|
|
}
|
|
|
|
// ErrDNSIPAddress indicates that you passed an IP address to a DNS
|
|
// function that only works with domain names.
|
|
var ErrDNSIPAddress = errors.New("ooresolver: expected domain, found IP address")
|
|
|
|
func (r *resolverShortCircuitIPAddr) LookupNS(
|
|
ctx context.Context, hostname string) ([]*net.NS, error) {
|
|
if net.ParseIP(hostname) != nil {
|
|
return nil, ErrDNSIPAddress
|
|
}
|
|
return r.Resolver.LookupNS(ctx, hostname)
|
|
}
|
|
|
|
// IsIPv6 returns true if the given candidate is a valid IP address
|
|
// representation and such representation is IPv6.
|
|
func IsIPv6(candidate string) (bool, error) {
|
|
if net.ParseIP(candidate) == nil {
|
|
return false, ErrInvalidIP
|
|
}
|
|
return isIPv6(candidate), nil
|
|
}
|
|
|
|
// isIPv6 returns true if the given IP address is IPv6.
|
|
func isIPv6(candidate string) bool {
|
|
// This check for identifying IPv6 is discussed
|
|
// at https://stackoverflow.com/questions/22751035
|
|
// and seems good-enough for our purposes.
|
|
return strings.Contains(candidate, ":")
|
|
}
|
|
|
|
// ErrNoResolver is the type of error returned by "without resolver"
|
|
// dialer when asked to dial for and endpoint containing a domain name,
|
|
// since they can only dial for endpoints containing IP addresses.
|
|
var ErrNoResolver = errors.New("no configured resolver")
|
|
|
|
// NullResolver is a resolver that is not capable of resolving
|
|
// domain names to IP addresses and always returns ErrNoResolver.
|
|
type NullResolver struct{}
|
|
|
|
func (r *NullResolver) LookupHost(ctx context.Context, hostname string) (addrs []string, err error) {
|
|
return nil, ErrNoResolver
|
|
}
|
|
|
|
func (r *NullResolver) Network() string {
|
|
return "null"
|
|
}
|
|
|
|
func (r *NullResolver) Address() string {
|
|
return ""
|
|
}
|
|
|
|
func (r *NullResolver) CloseIdleConnections() {
|
|
// nothing to do
|
|
}
|
|
|
|
func (r *NullResolver) LookupHTTPS(
|
|
ctx context.Context, domain string) (*model.HTTPSSvc, error) {
|
|
return nil, ErrNoResolver
|
|
}
|
|
|
|
func (r *NullResolver) LookupNS(
|
|
ctx context.Context, domain string) ([]*net.NS, error) {
|
|
return nil, ErrNoResolver
|
|
}
|
|
|
|
// resolverErrWrapper is a Resolver that knows about wrapping errors.
|
|
type resolverErrWrapper struct {
|
|
Resolver model.Resolver
|
|
}
|
|
|
|
var _ model.Resolver = &resolverErrWrapper{}
|
|
|
|
func (r *resolverErrWrapper) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
|
addrs, err := r.Resolver.LookupHost(ctx, hostname)
|
|
if err != nil {
|
|
return nil, NewErrWrapper(ClassifyResolverError, ResolveOperation, err)
|
|
}
|
|
return addrs, nil
|
|
}
|
|
|
|
func (r *resolverErrWrapper) LookupHTTPS(
|
|
ctx context.Context, domain string) (*model.HTTPSSvc, error) {
|
|
out, err := r.Resolver.LookupHTTPS(ctx, domain)
|
|
if err != nil {
|
|
return nil, NewErrWrapper(ClassifyResolverError, ResolveOperation, err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *resolverErrWrapper) Network() string {
|
|
return r.Resolver.Network()
|
|
}
|
|
|
|
func (r *resolverErrWrapper) Address() string {
|
|
return r.Resolver.Address()
|
|
}
|
|
|
|
func (r *resolverErrWrapper) CloseIdleConnections() {
|
|
r.Resolver.CloseIdleConnections()
|
|
}
|
|
|
|
func (r *resolverErrWrapper) LookupNS(
|
|
ctx context.Context, domain string) ([]*net.NS, error) {
|
|
out, err := r.Resolver.LookupNS(ctx, domain)
|
|
if err != nil {
|
|
return nil, NewErrWrapper(ClassifyResolverError, ResolveOperation, err)
|
|
}
|
|
return out, nil
|
|
}
|