-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(netxlite): introduce the getaddrinfo transport
This diff modifies the system resolver to use a getaddrinf transport. Obviously the transport is a fake, but its existence will allow us to observe DNS events more naturally. A lookup using the system resolver would be a ANY lookup that will contain all the resolved IP addresses into the same response. This change was also part of websteps-illustrated, albeit the way in which I did it there was less clean than what we have here. Ref issue: ooni/probe#2096
- Loading branch information
1 parent
7e0b473
commit 6209071
Showing
8 changed files
with
389 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package netxlite | ||
|
||
import ( | ||
"context" | ||
"net" | ||
"time" | ||
|
||
"github.com/miekg/dns" | ||
"github.com/ooni/probe-cli/v3/internal/model" | ||
) | ||
|
||
// dnsOverGetaddrinfoTransport is a DNSTransport using getaddrinfo. | ||
type dnsOverGetaddrinfoTransport struct { | ||
testableTimeout time.Duration | ||
testableLookupHost func(ctx context.Context, domain string) ([]string, error) | ||
} | ||
|
||
var _ model.DNSTransport = &dnsOverGetaddrinfoTransport{} | ||
|
||
func (txp *dnsOverGetaddrinfoTransport) RoundTrip( | ||
ctx context.Context, query model.DNSQuery) (model.DNSResponse, error) { | ||
if query.Type() != dns.TypeANY { | ||
return nil, ErrNoDNSTransport | ||
} | ||
addrs, err := txp.lookup(ctx, query.Domain()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
resp := &dnsOverGetaddrinfoResponse{ | ||
addrs: addrs, | ||
query: query, | ||
} | ||
return resp, nil | ||
} | ||
|
||
type dnsOverGetaddrinfoResponse struct { | ||
addrs []string | ||
query model.DNSQuery | ||
} | ||
|
||
func (txp *dnsOverGetaddrinfoTransport) lookup( | ||
ctx context.Context, hostname string) ([]string, error) { | ||
// This code forces adding a shorter timeout to the domain name | ||
// resolutions when using the system resolver. We have seen cases | ||
// in which such a timeout becomes too large. One such case is | ||
// described in https://github.com/ooni/probe/issues/1726. | ||
addrsch, errch := make(chan []string, 1), make(chan error, 1) | ||
ctx, cancel := context.WithTimeout(ctx, txp.timeout()) | ||
defer cancel() | ||
go func() { | ||
addrs, err := txp.lookupfn()(ctx, hostname) | ||
if err != nil { | ||
errch <- err | ||
return | ||
} | ||
addrsch <- addrs | ||
}() | ||
select { | ||
case <-ctx.Done(): | ||
return nil, ctx.Err() | ||
case addrs := <-addrsch: | ||
return addrs, nil | ||
case err := <-errch: | ||
return nil, err | ||
} | ||
} | ||
|
||
func (txp *dnsOverGetaddrinfoTransport) timeout() time.Duration { | ||
if txp.testableTimeout > 0 { | ||
return txp.testableTimeout | ||
} | ||
return 15 * time.Second | ||
} | ||
|
||
func (txp *dnsOverGetaddrinfoTransport) lookupfn() func(ctx context.Context, domain string) ([]string, error) { | ||
if txp.testableLookupHost != nil { | ||
return txp.testableLookupHost | ||
} | ||
return TProxy.DefaultResolver().LookupHost | ||
} | ||
|
||
func (txp *dnsOverGetaddrinfoTransport) RequiresPadding() bool { | ||
return false | ||
} | ||
|
||
func (txp *dnsOverGetaddrinfoTransport) Network() string { | ||
return TProxy.DefaultResolver().Network() | ||
} | ||
|
||
func (txp *dnsOverGetaddrinfoTransport) Address() string { | ||
return "" | ||
} | ||
|
||
func (txp *dnsOverGetaddrinfoTransport) CloseIdleConnections() { | ||
// nothing | ||
} | ||
|
||
func (r *dnsOverGetaddrinfoResponse) Query() model.DNSQuery { | ||
return r.query | ||
} | ||
|
||
func (r *dnsOverGetaddrinfoResponse) Bytes() []byte { | ||
return nil | ||
} | ||
|
||
func (r *dnsOverGetaddrinfoResponse) Rcode() int { | ||
return 0 | ||
} | ||
|
||
func (r *dnsOverGetaddrinfoResponse) DecodeHTTPS() (*model.HTTPSSvc, error) { | ||
return nil, ErrNoDNSTransport | ||
} | ||
|
||
func (r *dnsOverGetaddrinfoResponse) DecodeLookupHost() ([]string, error) { | ||
return r.addrs, nil | ||
} | ||
|
||
func (r *dnsOverGetaddrinfoResponse) DecodeNS() ([]*net.NS, error) { | ||
return nil, ErrNoDNSTransport | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
package netxlite | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"strings" | ||
"sync" | ||
"testing" | ||
"time" | ||
|
||
"github.com/miekg/dns" | ||
) | ||
|
||
func TestDNSOverGetadddrinfo(t *testing.T) { | ||
t.Run("RequiresPadding", func(t *testing.T) { | ||
txp := &dnsOverGetaddrinfoTransport{} | ||
if txp.RequiresPadding() { | ||
t.Fatal("expected false") | ||
} | ||
}) | ||
|
||
t.Run("Network", func(t *testing.T) { | ||
txp := &dnsOverGetaddrinfoTransport{} | ||
if txp.Network() != TProxy.DefaultResolver().Network() { | ||
t.Fatal("unexpected Network") | ||
} | ||
}) | ||
|
||
t.Run("Address", func(t *testing.T) { | ||
txp := &dnsOverGetaddrinfoTransport{} | ||
if txp.Address() != "" { | ||
t.Fatal("unexpected Address") | ||
} | ||
}) | ||
|
||
t.Run("CloseIdleConnections", func(t *testing.T) { | ||
txp := &dnsOverGetaddrinfoTransport{} | ||
txp.CloseIdleConnections() // does not crash | ||
}) | ||
|
||
t.Run("check default timeout", func(t *testing.T) { | ||
txp := &dnsOverGetaddrinfoTransport{} | ||
if txp.timeout() != 15*time.Second { | ||
t.Fatal("unexpected default timeout") | ||
} | ||
}) | ||
|
||
t.Run("check default lookup host func not nil", func(t *testing.T) { | ||
txp := &dnsOverGetaddrinfoTransport{} | ||
if txp.lookupfn() == nil { | ||
t.Fatal("expected non-nil func here") | ||
} | ||
}) | ||
|
||
t.Run("RoundTrip", func(t *testing.T) { | ||
t.Run("with invalid query type", func(t *testing.T) { | ||
txp := &dnsOverGetaddrinfoTransport{ | ||
testableLookupHost: func(ctx context.Context, domain string) ([]string, error) { | ||
return []string{"8.8.8.8"}, nil | ||
}, | ||
} | ||
encoder := &DNSEncoderMiekg{} | ||
query := encoder.Encode("dns.google", dns.TypeA, false) | ||
ctx := context.Background() | ||
resp, err := txp.RoundTrip(ctx, query) | ||
if !errors.Is(err, ErrNoDNSTransport) { | ||
t.Fatal("unexpected err", err) | ||
} | ||
if resp != nil { | ||
t.Fatal("expected nil resp") | ||
} | ||
}) | ||
|
||
t.Run("with success", func(t *testing.T) { | ||
txp := &dnsOverGetaddrinfoTransport{ | ||
testableLookupHost: func(ctx context.Context, domain string) ([]string, error) { | ||
return []string{"8.8.8.8"}, nil | ||
}, | ||
} | ||
encoder := &DNSEncoderMiekg{} | ||
query := encoder.Encode("dns.google", dns.TypeANY, false) | ||
ctx := context.Background() | ||
resp, err := txp.RoundTrip(ctx, query) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
addrs, err := resp.DecodeLookupHost() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if len(addrs) != 1 || addrs[0] != "8.8.8.8" { | ||
t.Fatal("invalid addrs") | ||
} | ||
if resp.Query() != query { | ||
t.Fatal("invalid query") | ||
} | ||
if len(resp.Bytes()) != 0 { | ||
t.Fatal("invalid response bytes") | ||
} | ||
if resp.Rcode() != 0 { | ||
t.Fatal("invalid rcode") | ||
} | ||
https, err := resp.DecodeHTTPS() | ||
if !errors.Is(err, ErrNoDNSTransport) { | ||
t.Fatal("unexpected err", err) | ||
} | ||
if https != nil { | ||
t.Fatal("expected nil https") | ||
} | ||
ns, err := resp.DecodeNS() | ||
if !errors.Is(err, ErrNoDNSTransport) { | ||
t.Fatal("unexpected err", err) | ||
} | ||
if len(ns) != 0 { | ||
t.Fatal("expected zero-length ns") | ||
} | ||
}) | ||
|
||
t.Run("with timeout and success", func(t *testing.T) { | ||
wg := &sync.WaitGroup{} | ||
wg.Add(1) | ||
done := make(chan interface{}) | ||
txp := &dnsOverGetaddrinfoTransport{ | ||
testableTimeout: 1 * time.Microsecond, | ||
testableLookupHost: func(ctx context.Context, domain string) ([]string, error) { | ||
defer wg.Done() | ||
<-done | ||
return []string{"8.8.8.8"}, nil | ||
}, | ||
} | ||
encoder := &DNSEncoderMiekg{} | ||
query := encoder.Encode("dns.google", dns.TypeANY, false) | ||
ctx := context.Background() | ||
resp, err := txp.RoundTrip(ctx, query) | ||
if !errors.Is(err, context.DeadlineExceeded) { | ||
t.Fatal("unexpected err", err) | ||
} | ||
if resp != nil { | ||
t.Fatal("invalid resp") | ||
} | ||
close(done) | ||
wg.Wait() | ||
}) | ||
|
||
t.Run("with timeout and failure", func(t *testing.T) { | ||
wg := &sync.WaitGroup{} | ||
wg.Add(1) | ||
done := make(chan interface{}) | ||
txp := &dnsOverGetaddrinfoTransport{ | ||
testableTimeout: 1 * time.Microsecond, | ||
testableLookupHost: func(ctx context.Context, domain string) ([]string, error) { | ||
defer wg.Done() | ||
<-done | ||
return nil, errors.New("no such host") | ||
}, | ||
} | ||
encoder := &DNSEncoderMiekg{} | ||
query := encoder.Encode("dns.google", dns.TypeANY, false) | ||
ctx := context.Background() | ||
resp, err := txp.RoundTrip(ctx, query) | ||
if !errors.Is(err, context.DeadlineExceeded) { | ||
t.Fatal("not the error we expected", err) | ||
} | ||
if resp != nil { | ||
t.Fatal("invalid resp") | ||
} | ||
close(done) | ||
wg.Wait() | ||
}) | ||
|
||
t.Run("with NXDOMAIN", func(t *testing.T) { | ||
txp := &dnsOverGetaddrinfoTransport{ | ||
testableLookupHost: func(ctx context.Context, domain string) ([]string, error) { | ||
return nil, ErrOODNSNoSuchHost | ||
}, | ||
} | ||
encoder := &DNSEncoderMiekg{} | ||
query := encoder.Encode("dns.google", dns.TypeANY, false) | ||
ctx := context.Background() | ||
resp, err := txp.RoundTrip(ctx, query) | ||
if err == nil || !strings.HasSuffix(err.Error(), "no such host") { | ||
t.Fatal("not the error we expected", err) | ||
} | ||
if resp != nil { | ||
t.Fatal("invalid resp") | ||
} | ||
}) | ||
|
||
}) | ||
} |
Oops, something went wrong.