fix(netxlite/dns): more stricly mirror stdlib error strings (#513)

This diff attempts to modify the errors reported by our custom
resolver by matching more strings from the stdlib.

Part of https://github.com/ooni/probe/issues/1733 and diff has been
extracted from https://github.com/ooni/probe-cli/pull/506.
This commit is contained in:
Simone Basso
2021-09-27 16:48:46 +02:00
committed by GitHub
parent 201f602a40
commit 24b230fd38
17 changed files with 177 additions and 52 deletions
+19 -11
View File
@@ -1,34 +1,42 @@
package dnsx
import (
"errors"
"github.com/miekg/dns"
"github.com/ooni/probe-cli/v3/internal/netxlite/errorsx"
)
// The Decoder decodes a DNS reply into A or AAAA entries. It will use the
// provided qtype and only look for mathing entries. It will return error if
// there are no entries for the requested qtype inside the reply.
// The Decoder decodes DNS replies.
type Decoder interface {
Decode(qtype uint16, data []byte) ([]string, error)
// DecodeLookupHost decodes an A or AAAA reply.
DecodeLookupHost(qtype uint16, data []byte) ([]string, error)
}
// MiekgDecoder uses github.com/miekg/dns to implement the Decoder.
type MiekgDecoder struct{}
// Decode implements Decoder.Decode.
func (d *MiekgDecoder) Decode(qtype uint16, data []byte) ([]string, error) {
func (d *MiekgDecoder) parseReply(data []byte) (*dns.Msg, error) {
reply := new(dns.Msg)
if err := reply.Unpack(data); err != nil {
return nil, err
}
// TODO(bassosimone): map more errors to net.DNSError names
// TODO(bassosimone): add support for lame referral.
switch reply.Rcode {
case dns.RcodeSuccess:
return reply, nil
case dns.RcodeNameError:
return nil, errors.New("ooniresolver: no such host")
return nil, errorsx.ErrOODNSNoSuchHost
case dns.RcodeRefused:
return nil, errorsx.ErrOODNSRefused
default:
return nil, errors.New("ooniresolver: query failed")
return nil, errorsx.ErrOODNSMisbehaving
}
}
func (d *MiekgDecoder) DecodeLookupHost(qtype uint16, data []byte) ([]string, error) {
reply, err := d.parseReply(data)
if err != nil {
return nil, err
}
var addrs []string
for _, answer := range reply.Answer {
@@ -46,7 +54,7 @@ func (d *MiekgDecoder) Decode(qtype uint16, data []byte) ([]string, error) {
}
}
if len(addrs) <= 0 {
return nil, errors.New("ooniresolver: no response returned")
return nil, errorsx.ErrOODNSNoAnswer
}
return addrs, nil
}
+37 -18
View File
@@ -1,16 +1,18 @@
package dnsx
import (
"errors"
"net"
"strings"
"testing"
"github.com/miekg/dns"
"github.com/ooni/probe-cli/v3/internal/netxlite/errorsx"
)
func TestDecoderUnpackError(t *testing.T) {
d := &MiekgDecoder{}
data, err := d.Decode(dns.TypeA, nil)
data, err := d.DecodeLookupHost(dns.TypeA, nil)
if err == nil {
t.Fatal("expected an error here")
}
@@ -21,20 +23,20 @@ func TestDecoderUnpackError(t *testing.T) {
func TestDecoderNXDOMAIN(t *testing.T) {
d := &MiekgDecoder{}
data, err := d.Decode(dns.TypeA, genReplyError(t, dns.RcodeNameError))
data, err := d.DecodeLookupHost(dns.TypeA, genReplyError(t, dns.RcodeNameError))
if err == nil || !strings.HasSuffix(err.Error(), "no such host") {
t.Fatal("not the error we expected")
t.Fatal("not the error we expected", err)
}
if data != nil {
t.Fatal("expected nil data here")
}
}
func TestDecoderOtherError(t *testing.T) {
func TestDecoderRefusedError(t *testing.T) {
d := &MiekgDecoder{}
data, err := d.Decode(dns.TypeA, genReplyError(t, dns.RcodeRefused))
if err == nil || !strings.HasSuffix(err.Error(), "query failed") {
t.Fatal("not the error we expected")
data, err := d.DecodeLookupHost(dns.TypeA, genReplyError(t, dns.RcodeRefused))
if !errors.Is(err, errorsx.ErrOODNSRefused) {
t.Fatal("not the error we expected", err)
}
if data != nil {
t.Fatal("expected nil data here")
@@ -43,9 +45,9 @@ func TestDecoderOtherError(t *testing.T) {
func TestDecoderNoAddress(t *testing.T) {
d := &MiekgDecoder{}
data, err := d.Decode(dns.TypeA, genReplySuccess(t, dns.TypeA))
if err == nil || !strings.HasSuffix(err.Error(), "no response returned") {
t.Fatal("not the error we expected")
data, err := d.DecodeLookupHost(dns.TypeA, genReplySuccess(t, dns.TypeA))
if !errors.Is(err, errorsx.ErrOODNSNoAnswer) {
t.Fatal("not the error we expected", err)
}
if data != nil {
t.Fatal("expected nil data here")
@@ -54,7 +56,7 @@ func TestDecoderNoAddress(t *testing.T) {
func TestDecoderDecodeA(t *testing.T) {
d := &MiekgDecoder{}
data, err := d.Decode(
data, err := d.DecodeLookupHost(
dns.TypeA, genReplySuccess(t, dns.TypeA, "1.1.1.1", "8.8.8.8"))
if err != nil {
t.Fatal(err)
@@ -72,7 +74,7 @@ func TestDecoderDecodeA(t *testing.T) {
func TestDecoderDecodeAAAA(t *testing.T) {
d := &MiekgDecoder{}
data, err := d.Decode(
data, err := d.DecodeLookupHost(
dns.TypeAAAA, genReplySuccess(t, dns.TypeAAAA, "::1", "fe80::1"))
if err != nil {
t.Fatal(err)
@@ -90,10 +92,10 @@ func TestDecoderDecodeAAAA(t *testing.T) {
func TestDecoderUnexpectedAReply(t *testing.T) {
d := &MiekgDecoder{}
data, err := d.Decode(
data, err := d.DecodeLookupHost(
dns.TypeA, genReplySuccess(t, dns.TypeAAAA, "::1", "fe80::1"))
if err == nil || !strings.HasSuffix(err.Error(), "no response returned") {
t.Fatal("not the error we expected")
if !errors.Is(err, errorsx.ErrOODNSNoAnswer) {
t.Fatal("not the error we expected", err)
}
if data != nil {
t.Fatal("expected nil data here")
@@ -102,10 +104,10 @@ func TestDecoderUnexpectedAReply(t *testing.T) {
func TestDecoderUnexpectedAAAAReply(t *testing.T) {
d := &MiekgDecoder{}
data, err := d.Decode(
data, err := d.DecodeLookupHost(
dns.TypeAAAA, genReplySuccess(t, dns.TypeA, "1.1.1.1", "8.8.4.4."))
if err == nil || !strings.HasSuffix(err.Error(), "no response returned") {
t.Fatal("not the error we expected")
if !errors.Is(err, errorsx.ErrOODNSNoAnswer) {
t.Fatal("not the error we expected", err)
}
if data != nil {
t.Fatal("expected nil data here")
@@ -179,3 +181,20 @@ func genReplySuccess(t *testing.T, qtype uint16, ips ...string) []byte {
}
return data
}
func TestParseReply(t *testing.T) {
d := &MiekgDecoder{}
msg := &dns.Msg{}
msg.Rcode = dns.RcodeFormatError // an rcode we don't handle
data, err := msg.Pack()
if err != nil {
t.Fatal(err)
}
reply, err := d.parseReply(data)
if !errors.Is(err, errorsx.ErrOODNSMisbehaving) { // catch all error
t.Fatal("not the error we expected", err)
}
if reply != nil {
t.Fatal("expected nil reply")
}
}
+2 -2
View File
@@ -27,14 +27,14 @@ type DNSOverHTTPS struct {
// NewDNSOverHTTPS creates a new DNSOverHTTP instance from the
// specified http.Client and URL, as a convenience.
func NewDNSOverHTTPS(client *http.Client, URL string) *DNSOverHTTPS {
func NewDNSOverHTTPS(client HTTPClient, URL string) *DNSOverHTTPS {
return NewDNSOverHTTPSWithHostOverride(client, URL, "")
}
// NewDNSOverHTTPSWithHostOverride is like NewDNSOverHTTPS except that
// it's creating a resolver where we use the specified host.
func NewDNSOverHTTPSWithHostOverride(
client *http.Client, URL, hostOverride string) *DNSOverHTTPS {
client HTTPClient, URL, hostOverride string) *DNSOverHTTPS {
return &DNSOverHTTPS{Client: client, URL: URL, HostOverride: hostOverride}
}
@@ -175,3 +175,18 @@ func TestDNSOverHTTPSHostOverride(t *testing.T) {
t.Fatal("did not see correct host override")
}
}
func TestDNSOverHTTPSCloseIdleConnections(t *testing.T) {
var called bool
doh := &DNSOverHTTPS{
Client: &mocks.HTTPClient{
MockCloseIdleConnections: func() {
called = true
},
},
}
doh.CloseIdleConnections()
if !called {
t.Fatal("not called")
}
}
+8 -6
View File
@@ -51,8 +51,8 @@ func (r *SerialResolver) CloseIdleConnections() {
// LookupHost implements Resolver.LookupHost.
func (r *SerialResolver) LookupHost(ctx context.Context, hostname string) ([]string, error) {
var addrs []string
addrsA, errA := r.roundTripWithRetry(ctx, hostname, dns.TypeA)
addrsAAAA, errAAAA := r.roundTripWithRetry(ctx, hostname, dns.TypeAAAA)
addrsA, errA := r.lookupHostWithRetry(ctx, hostname, dns.TypeA)
addrsAAAA, errAAAA := r.lookupHostWithRetry(ctx, hostname, dns.TypeAAAA)
if errA != nil && errAAAA != nil {
return nil, errA
}
@@ -61,11 +61,11 @@ func (r *SerialResolver) LookupHost(ctx context.Context, hostname string) ([]str
return addrs, nil
}
func (r *SerialResolver) roundTripWithRetry(
func (r *SerialResolver) lookupHostWithRetry(
ctx context.Context, hostname string, qtype uint16) ([]string, error) {
var errorslist []error
for i := 0; i < 3; i++ {
replies, err := r.roundTrip(ctx, hostname, qtype)
replies, err := r.lookupHostWithoutRetry(ctx, hostname, qtype)
if err == nil {
return replies, nil
}
@@ -87,7 +87,9 @@ func (r *SerialResolver) roundTripWithRetry(
return nil, errorslist[0]
}
func (r *SerialResolver) roundTrip(
// lookupHostWithoutRetry issues a lookup host query for the specified
// qtype (dns.A or dns.AAAA) without retrying on failure.
func (r *SerialResolver) lookupHostWithoutRetry(
ctx context.Context, hostname string, qtype uint16) ([]string, error) {
querydata, err := r.Encoder.Encode(hostname, qtype, r.Txp.RequiresPadding())
if err != nil {
@@ -97,5 +99,5 @@ func (r *SerialResolver) roundTrip(
if err != nil {
return nil, err
}
return r.Decoder.Decode(qtype, replydata)
return r.Decoder.DecodeLookupHost(qtype, replydata)
}
+17 -3
View File
@@ -5,7 +5,6 @@ import (
"crypto/tls"
"errors"
"net"
"strings"
"testing"
"github.com/miekg/dns"
@@ -79,8 +78,8 @@ func TestOONIWithEmptyReply(t *testing.T) {
}
r := NewSerialResolver(txp)
addrs, err := r.LookupHost(context.Background(), "www.gogle.com")
if err == nil || !strings.HasSuffix(err.Error(), "no response returned") {
t.Fatal("not the error we expected")
if !errors.Is(err, errorsx.ErrOODNSNoAnswer) {
t.Fatal("not the error we expected", err)
}
if addrs != nil {
t.Fatal("expected nil address here")
@@ -146,3 +145,18 @@ func TestOONIWithTimeout(t *testing.T) {
t.Fatal("we didn't actually take the timeouts")
}
}
func TestSerialResolverCloseIdleConnections(t *testing.T) {
var called bool
r := &SerialResolver{
Txp: &mocks.RoundTripper{
MockCloseIdleConnections: func() {
called = true
},
},
}
r.CloseIdleConnections()
if !called {
t.Fatal("not called")
}
}