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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user