refactor(netx): move construction logic outside package (#798)

For testability, replace most if-based construction logic with
calls to well-tested factories living in other packages.

While there, acknowledge that a bunch of types could now be private
and make them private, modifying the code to call the public
factories allowing to construct said types instead.

Part of https://github.com/ooni/probe/issues/2121
This commit is contained in:
Simone Basso 2022-06-05 21:22:27 +02:00 committed by GitHub
parent 2d3d5d9cdc
commit 6b85dfce88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 476 additions and 185 deletions

View File

@ -6,8 +6,8 @@ package bytecounter
import "net" import "net"
// Conn wraps a network connection and counts bytes. // wrappedConn wraps a network connection and counts bytes.
type Conn struct { type wrappedConn struct {
// net.Conn is the underlying net.Conn. // net.Conn is the underlying net.Conn.
net.Conn net.Conn
@ -16,28 +16,28 @@ type Conn struct {
} }
// Read implements net.Conn.Read. // Read implements net.Conn.Read.
func (c *Conn) Read(p []byte) (int, error) { func (c *wrappedConn) Read(p []byte) (int, error) {
count, err := c.Conn.Read(p) count, err := c.Conn.Read(p)
c.Counter.CountBytesReceived(count) c.Counter.CountBytesReceived(count)
return count, err return count, err
} }
// Write implements net.Conn.Write. // Write implements net.Conn.Write.
func (c *Conn) Write(p []byte) (int, error) { func (c *wrappedConn) Write(p []byte) (int, error) {
count, err := c.Conn.Write(p) count, err := c.Conn.Write(p)
c.Counter.CountBytesSent(count) c.Counter.CountBytesSent(count)
return count, err return count, err
} }
// Wrap returns a new conn that uses the given counter. // WrapConn returns a new conn that uses the given counter.
func Wrap(conn net.Conn, counter *Counter) net.Conn { func WrapConn(conn net.Conn, counter *Counter) net.Conn {
return &Conn{Conn: conn, Counter: counter} return &wrappedConn{Conn: conn, Counter: counter}
} }
// MaybeWrap is like wrap if counter is not nil, otherwise it's a no-op. // MaybeWrapConn is like wrap if counter is not nil, otherwise it's a no-op.
func MaybeWrap(conn net.Conn, counter *Counter) net.Conn { func MaybeWrapConn(conn net.Conn, counter *Counter) net.Conn {
if counter == nil { if counter == nil {
return conn return conn
} }
return Wrap(conn, counter) return WrapConn(conn, counter)
} }

View File

@ -7,7 +7,7 @@ import (
"github.com/ooni/probe-cli/v3/internal/model/mocks" "github.com/ooni/probe-cli/v3/internal/model/mocks"
) )
func TestConnWorksOnSuccess(t *testing.T) { func TestWrappedConnWorksOnSuccess(t *testing.T) {
counter := New() counter := New()
underlying := &mocks.Conn{ underlying := &mocks.Conn{
MockRead: func(b []byte) (int, error) { MockRead: func(b []byte) (int, error) {
@ -17,7 +17,7 @@ func TestConnWorksOnSuccess(t *testing.T) {
return 4, nil return 4, nil
}, },
} }
conn := &Conn{ conn := &wrappedConn{
Conn: underlying, Conn: underlying,
Counter: counter, Counter: counter,
} }
@ -35,7 +35,7 @@ func TestConnWorksOnSuccess(t *testing.T) {
} }
} }
func TestConnWorksOnFailure(t *testing.T) { func TestWrappedConnWorksOnFailure(t *testing.T) {
readError := errors.New("read error") readError := errors.New("read error")
writeError := errors.New("write error") writeError := errors.New("write error")
counter := New() counter := New()
@ -47,7 +47,7 @@ func TestConnWorksOnFailure(t *testing.T) {
return 0, writeError return 0, writeError
}, },
} }
conn := &Conn{ conn := &wrappedConn{
Conn: underlying, Conn: underlying,
Counter: counter, Counter: counter,
} }
@ -65,20 +65,20 @@ func TestConnWorksOnFailure(t *testing.T) {
} }
} }
func TestWrap(t *testing.T) { func TestWrapConn(t *testing.T) {
conn := &mocks.Conn{} conn := &mocks.Conn{}
counter := New() counter := New()
nconn := Wrap(conn, counter) nconn := WrapConn(conn, counter)
_, good := nconn.(*Conn) _, good := nconn.(*wrappedConn)
if !good { if !good {
t.Fatal("did not wrap") t.Fatal("did not wrap")
} }
} }
func TestMaybeWrap(t *testing.T) { func TestMaybeWrapConn(t *testing.T) {
t.Run("with nil counter", func(t *testing.T) { t.Run("with nil counter", func(t *testing.T) {
conn := &mocks.Conn{} conn := &mocks.Conn{}
nconn := MaybeWrap(conn, nil) nconn := MaybeWrapConn(conn, nil)
_, good := nconn.(*mocks.Conn) _, good := nconn.(*mocks.Conn)
if !good { if !good {
t.Fatal("did not wrap") t.Fatal("did not wrap")
@ -88,8 +88,8 @@ func TestMaybeWrap(t *testing.T) {
t.Run("with legit counter", func(t *testing.T) { t.Run("with legit counter", func(t *testing.T) {
conn := &mocks.Conn{} conn := &mocks.Conn{}
counter := New() counter := New()
nconn := MaybeWrap(conn, counter) nconn := MaybeWrapConn(conn, counter)
_, good := nconn.(*Conn) _, good := nconn.(*wrappedConn)
if !good { if !good {
t.Fatal("did not wrap") t.Fatal("did not wrap")
} }

View File

@ -38,7 +38,7 @@ func WithExperimentByteCounter(ctx context.Context, counter *Counter) context.Co
// MaybeWrapWithContextByteCounters wraps a conn with the byte counters // MaybeWrapWithContextByteCounters wraps a conn with the byte counters
// that have previosuly been configured into a context. // that have previosuly been configured into a context.
func MaybeWrapWithContextByteCounters(ctx context.Context, conn net.Conn) net.Conn { func MaybeWrapWithContextByteCounters(ctx context.Context, conn net.Conn) net.Conn {
conn = MaybeWrap(conn, ContextExperimentByteCounter(ctx)) conn = MaybeWrapConn(conn, ContextExperimentByteCounter(ctx))
conn = MaybeWrap(conn, ContextSessionByteCounter(ctx)) conn = MaybeWrapConn(conn, ContextSessionByteCounter(ctx))
return conn return conn
} }

View File

@ -2,7 +2,7 @@ package bytecounter
import "testing" import "testing"
func TestGood(t *testing.T) { func TestCounter(t *testing.T) {
counter := New() counter := New()
counter.CountBytesReceived(16384) counter.CountBytesReceived(16384)
counter.CountKibiBytesReceived(10) counter.CountKibiBytesReceived(10)

View File

@ -11,8 +11,8 @@ import (
"github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model"
) )
// ContextAwareDialer is a model.Dialer that attempts to count bytes using // MaybeWrapWithContextAwareDialer wraps the given dialer with a ContextAwareDialer
// the MaybeWrapWithContextByteCounters function. // if the enabled argument is true and otherwise just returns the given dialer.
// //
// Bug // Bug
// //
@ -24,19 +24,29 @@ import (
// //
// For this reason, this implementation may be heavily changed/removed // For this reason, this implementation may be heavily changed/removed
// in the future (<- this message is now ~two years old, though). // in the future (<- this message is now ~two years old, though).
type ContextAwareDialer struct { func MaybeWrapWithContextAwareDialer(enabled bool, dialer model.Dialer) model.Dialer {
if !enabled {
return dialer
}
return WrapWithContextAwareDialer(dialer)
}
// contextAwareDialer is a model.Dialer that attempts to count bytes using
// the MaybeWrapWithContextByteCounters function.
type contextAwareDialer struct {
Dialer model.Dialer Dialer model.Dialer
} }
// NewContextAwareDialer creates a new ContextAwareDialer. // WrapWithContextAwareDialer creates a new ContextAwareDialer. See the docs
func NewContextAwareDialer(dialer model.Dialer) *ContextAwareDialer { // of MaybeWrapWithContextAwareDialer for a list of caveats.
return &ContextAwareDialer{Dialer: dialer} func WrapWithContextAwareDialer(dialer model.Dialer) *contextAwareDialer {
return &contextAwareDialer{Dialer: dialer}
} }
var _ model.Dialer = &ContextAwareDialer{} var _ model.Dialer = &contextAwareDialer{}
// DialContext implements Dialer.DialContext // DialContext implements Dialer.DialContext
func (d *ContextAwareDialer) DialContext( func (d *contextAwareDialer) DialContext(
ctx context.Context, network, address string) (net.Conn, error) { ctx context.Context, network, address string) (net.Conn, error) {
conn, err := d.Dialer.DialContext(ctx, network, address) conn, err := d.Dialer.DialContext(ctx, network, address)
if err != nil { if err != nil {
@ -47,6 +57,6 @@ func (d *ContextAwareDialer) DialContext(
} }
// CloseIdleConnections implements Dialer.CloseIdleConnections. // CloseIdleConnections implements Dialer.CloseIdleConnections.
func (d *ContextAwareDialer) CloseIdleConnections() { func (d *contextAwareDialer) CloseIdleConnections() {
d.Dialer.CloseIdleConnections() d.Dialer.CloseIdleConnections()
} }

View File

@ -10,6 +10,25 @@ import (
"github.com/ooni/probe-cli/v3/internal/model/mocks" "github.com/ooni/probe-cli/v3/internal/model/mocks"
) )
func TestMaybeWrapWithContextAwareDialer(t *testing.T) {
t.Run("when enabled is true", func(t *testing.T) {
underlying := &mocks.Dialer{}
dialer := MaybeWrapWithContextAwareDialer(true, underlying)
realDialer := dialer.(*contextAwareDialer)
if realDialer.Dialer != underlying {
t.Fatal("did not wrap correctly")
}
})
t.Run("when enabled is false", func(t *testing.T) {
underlying := &mocks.Dialer{}
dialer := MaybeWrapWithContextAwareDialer(false, underlying)
if dialer != underlying {
t.Fatal("unexpected result")
}
})
}
func TestContextAwareDialer(t *testing.T) { func TestContextAwareDialer(t *testing.T) {
t.Run("DialContext", func(t *testing.T) { t.Run("DialContext", func(t *testing.T) {
dialAndUseConn := func(ctx context.Context, bufsiz int) error { dialAndUseConn := func(ctx context.Context, bufsiz int) error {
@ -26,7 +45,7 @@ func TestContextAwareDialer(t *testing.T) {
return childConn, nil return childConn, nil
}, },
} }
dialer := NewContextAwareDialer(child) dialer := WrapWithContextAwareDialer(child)
conn, err := dialer.DialContext(ctx, "tcp", "10.0.0.1:443") conn, err := dialer.DialContext(ctx, "tcp", "10.0.0.1:443")
if err != nil { if err != nil {
return err return err
@ -68,7 +87,7 @@ func TestContextAwareDialer(t *testing.T) {
}) })
t.Run("failure", func(t *testing.T) { t.Run("failure", func(t *testing.T) {
dialer := &ContextAwareDialer{ dialer := &contextAwareDialer{
Dialer: &mocks.Dialer{ Dialer: &mocks.Dialer{
MockDialContext: func(ctx context.Context, network string, address string) (net.Conn, error) { MockDialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
return nil, io.EOF return nil, io.EOF
@ -92,7 +111,7 @@ func TestContextAwareDialer(t *testing.T) {
called = true called = true
}, },
} }
dialer := NewContextAwareDialer(child) dialer := WrapWithContextAwareDialer(child)
dialer.CloseIdleConnections() dialer.CloseIdleConnections()
if !called { if !called {
t.Fatal("not called") t.Fatal("not called")

View File

@ -7,29 +7,39 @@ import (
"github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model"
) )
// HTTPTransport is a model.HTTPTransport that counts bytes. // MaybeWrapHTTPTransport takes in input an HTTPTransport and either wraps it
type HTTPTransport struct { // to perform byte counting, if this counter is not nil, or just returns to the
// caller the original transport, when the counter is nil.
func (c *Counter) MaybeWrapHTTPTransport(txp model.HTTPTransport) model.HTTPTransport {
if c != nil {
txp = WrapHTTPTransport(txp, c)
}
return txp
}
// httpTransport is a model.HTTPTransport that counts bytes.
type httpTransport struct {
HTTPTransport model.HTTPTransport HTTPTransport model.HTTPTransport
Counter *Counter Counter *Counter
} }
// NewHTTPTransport creates a new byte-counting-aware HTTP transport. // WrapHTTPTransport creates a new byte-counting-aware HTTP transport.
func NewHTTPTransport(txp model.HTTPTransport, counter *Counter) model.HTTPTransport { func WrapHTTPTransport(txp model.HTTPTransport, counter *Counter) model.HTTPTransport {
return &HTTPTransport{ return &httpTransport{
HTTPTransport: txp, HTTPTransport: txp,
Counter: counter, Counter: counter,
} }
} }
var _ model.HTTPTransport = &HTTPTransport{} var _ model.HTTPTransport = &httpTransport{}
// CloseIdleConnections implements model.HTTPTransport.CloseIdleConnections. // CloseIdleConnections implements model.HTTPTransport.CloseIdleConnections.
func (txp *HTTPTransport) CloseIdleConnections() { func (txp *httpTransport) CloseIdleConnections() {
txp.HTTPTransport.CloseIdleConnections() txp.HTTPTransport.CloseIdleConnections()
} }
// RoundTrip implements model.HTTPTRansport.RoundTrip // RoundTrip implements model.HTTPTRansport.RoundTrip
func (txp *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { func (txp *httpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Body != nil { if req.Body != nil {
req.Body = &httpBodyWrapper{ req.Body = &httpBodyWrapper{
account: txp.Counter.CountBytesSent, account: txp.Counter.CountBytesSent,
@ -50,11 +60,11 @@ func (txp *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
} }
// Network implements model.HTTPTransport.Network. // Network implements model.HTTPTransport.Network.
func (txp *HTTPTransport) Network() string { func (txp *httpTransport) Network() string {
return txp.HTTPTransport.Network() return txp.HTTPTransport.Network()
} }
func (txp *HTTPTransport) estimateRequestMetadata(req *http.Request) { func (txp *httpTransport) estimateRequestMetadata(req *http.Request) {
txp.Counter.CountBytesSent(len(req.Method)) txp.Counter.CountBytesSent(len(req.Method))
txp.Counter.CountBytesSent(len(req.URL.String())) txp.Counter.CountBytesSent(len(req.URL.String()))
for key, values := range req.Header { for key, values := range req.Header {
@ -68,7 +78,7 @@ func (txp *HTTPTransport) estimateRequestMetadata(req *http.Request) {
txp.Counter.CountBytesSent(len("\r\n")) txp.Counter.CountBytesSent(len("\r\n"))
} }
func (txp *HTTPTransport) estimateResponseMetadata(resp *http.Response) { func (txp *httpTransport) estimateResponseMetadata(resp *http.Response) {
txp.Counter.CountBytesReceived(len(resp.Status)) txp.Counter.CountBytesReceived(len(resp.Status))
for key, values := range resp.Header { for key, values := range resp.Header {
for _, value := range values { for _, value := range values {

View File

@ -12,11 +12,32 @@ import (
"github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/netxlite"
) )
func TestMaybeWrapHTTPTransport(t *testing.T) {
t.Run("when counter is not nil", func(t *testing.T) {
underlying := &mocks.HTTPTransport{}
counter := &Counter{}
txp := counter.MaybeWrapHTTPTransport(underlying)
realTxp := txp.(*httpTransport)
if realTxp.HTTPTransport != underlying {
t.Fatal("did not wrap correctly")
}
})
t.Run("when counter is nil", func(t *testing.T) {
underlying := &mocks.HTTPTransport{}
var counter *Counter
txp := counter.MaybeWrapHTTPTransport(underlying)
if txp != underlying {
t.Fatal("unexpected result")
}
})
}
func TestHTTPTransport(t *testing.T) { func TestHTTPTransport(t *testing.T) {
t.Run("RoundTrip", func(t *testing.T) { t.Run("RoundTrip", func(t *testing.T) {
t.Run("failure", func(t *testing.T) { t.Run("failure", func(t *testing.T) {
counter := New() counter := New()
txp := &HTTPTransport{ txp := &httpTransport{
Counter: counter, Counter: counter,
HTTPTransport: &mocks.HTTPTransport{ HTTPTransport: &mocks.HTTPTransport{
MockRoundTrip: func(req *http.Request) (*http.Response, error) { MockRoundTrip: func(req *http.Request) (*http.Response, error) {
@ -47,7 +68,7 @@ func TestHTTPTransport(t *testing.T) {
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
counter := New() counter := New()
txp := &HTTPTransport{ txp := &httpTransport{
Counter: counter, Counter: counter,
HTTPTransport: &mocks.HTTPTransport{ HTTPTransport: &mocks.HTTPTransport{
MockRoundTrip: func(req *http.Request) (*http.Response, error) { MockRoundTrip: func(req *http.Request) (*http.Response, error) {
@ -91,7 +112,7 @@ func TestHTTPTransport(t *testing.T) {
t.Run("success with EOF", func(t *testing.T) { t.Run("success with EOF", func(t *testing.T) {
counter := New() counter := New()
txp := &HTTPTransport{ txp := &httpTransport{
Counter: counter, Counter: counter,
HTTPTransport: &mocks.HTTPTransport{ HTTPTransport: &mocks.HTTPTransport{
MockRoundTrip: func(req *http.Request) (*http.Response, error) { MockRoundTrip: func(req *http.Request) (*http.Response, error) {
@ -139,7 +160,7 @@ func TestHTTPTransport(t *testing.T) {
}, },
} }
counter := New() counter := New()
txp := NewHTTPTransport(child, counter) txp := WrapHTTPTransport(child, counter)
txp.CloseIdleConnections() txp.CloseIdleConnections()
if !called { if !called {
t.Fatal("not called") t.Fatal("not called")
@ -154,7 +175,7 @@ func TestHTTPTransport(t *testing.T) {
}, },
} }
counter := New() counter := New()
txp := NewHTTPTransport(child, counter) txp := WrapHTTPTransport(child, counter)
if network := txp.Network(); network != expected { if network := txp.Network(); network != expected {
t.Fatal("unexpected network", network) t.Fatal("unexpected network", network)
} }

View File

@ -285,10 +285,10 @@ func (e *Experiment) OpenReportContext(ctx context.Context) error {
} }
// use custom client to have proper byte accounting // use custom client to have proper byte accounting
httpClient := &http.Client{ httpClient := &http.Client{
Transport: &bytecounter.HTTPTransport{ Transport: bytecounter.WrapHTTPTransport(
HTTPTransport: e.session.httpDefaultTransport, // proxy is OK e.session.httpDefaultTransport, // proxy is OK
Counter: e.byteCounter, e.byteCounter,
}, ),
} }
client, err := e.session.NewProbeServicesClient(ctx) client, err := e.session.NewProbeServicesClient(ctx)
if err != nil { if err != nil {

View File

@ -32,7 +32,7 @@ func newDialManager(ndt7URL string, logger model.Logger, userAgent string) dialM
func (mgr dialManager) dialWithTestName(ctx context.Context, testName string) (*websocket.Conn, error) { func (mgr dialManager) dialWithTestName(ctx context.Context, testName string) (*websocket.Conn, error) {
reso := netxlite.NewResolverStdlib(mgr.logger) reso := netxlite.NewResolverStdlib(mgr.logger)
dlr := netxlite.NewDialerWithResolver(mgr.logger, reso) dlr := netxlite.NewDialerWithResolver(mgr.logger, reso)
dlr = bytecounter.NewContextAwareDialer(dlr) dlr = bytecounter.WrapWithContextAwareDialer(dlr)
// Implements shaping if the user builds using `-tags shaping` // Implements shaping if the user builds using `-tags shaping`
// See https://github.com/ooni/probe/issues/2112 // See https://github.com/ooni/probe/issues/2112
dlr = netxlite.NewMaybeShapingDialer(dlr) dlr = netxlite.NewMaybeShapingDialer(dlr)

View File

@ -2,44 +2,90 @@ package netx
import ( import (
"context" "context"
"net"
"sync" "sync"
"github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
) )
// CacheResolver is a resolver that caches successful replies. // MaybeWrapWithCachingResolver wraps the provided resolver with a resolver
type CacheResolver struct { // that remembers the result of previous successful resolutions, if the enabled
ReadOnly bool // argument is true. Otherwise, we return the unmodified provided resolver.
model.Resolver //
mu sync.Mutex // Bug: the returned resolver only applies caching to LookupHost and any other
cache map[string][]string // lookup operation returns ErrNoDNSTransport to the caller.
func MaybeWrapWithCachingResolver(enabled bool, reso model.Resolver) model.Resolver {
if enabled {
reso = &cacheResolver{
cache: map[string][]string{},
mu: sync.Mutex{},
readOnly: false,
resolver: reso,
}
}
return reso
} }
// LookupHost implements Resolver.LookupHost // MaybeWrapWithStaticDNSCache wraps the provided resolver with a resolver that
func (r *CacheResolver) LookupHost( // checks the given cache before issuing queries to the underlying DNS resolver.
//
// Bug: the returned resolver only applies caching to LookupHost and any other
// lookup operation returns ErrNoDNSTransport to the caller.
func MaybeWrapWithStaticDNSCache(cache map[string][]string, reso model.Resolver) model.Resolver {
if len(cache) > 0 {
reso = &cacheResolver{
cache: cache,
mu: sync.Mutex{},
readOnly: true,
resolver: reso,
}
}
return reso
}
// cacheResolver implements CachingResolver and StaticDNSCache.
type cacheResolver struct {
// cache is the underlying DNS cache.
cache map[string][]string
// mu provides mutual exclusion.
mu sync.Mutex
// readOnly means that we won't cache the result of successful resolutions.
readOnly bool
// resolver is the underlying resolver.
resolver model.Resolver
}
var _ model.Resolver = &cacheResolver{}
// LookupHost implements model.Resolver.LookupHost
func (r *cacheResolver) LookupHost(
ctx context.Context, hostname string) ([]string, error) { ctx context.Context, hostname string) ([]string, error) {
if entry := r.Get(hostname); entry != nil { if entry := r.get(hostname); entry != nil {
return entry, nil return entry, nil
} }
entry, err := r.Resolver.LookupHost(ctx, hostname) entry, err := r.resolver.LookupHost(ctx, hostname)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !r.ReadOnly { if !r.readOnly {
r.Set(hostname, entry) r.set(hostname, entry)
} }
return entry, nil return entry, nil
} }
// Get gets the currently configured entry for domain, or nil // get gets the currently configured entry for domain, or nil
func (r *CacheResolver) Get(domain string) []string { func (r *cacheResolver) get(domain string) []string {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
return r.cache[domain] return r.cache[domain]
} }
// Set allows to pre-populate the cache // set sets a valid inside the cache iff readOnly is false.
func (r *CacheResolver) Set(domain string, addresses []string) { func (r *cacheResolver) set(domain string, addresses []string) {
r.mu.Lock() r.mu.Lock()
if r.cache == nil { if r.cache == nil {
r.cache = make(map[string][]string) r.cache = make(map[string][]string)
@ -47,3 +93,28 @@ func (r *CacheResolver) Set(domain string, addresses []string) {
r.cache[domain] = addresses r.cache[domain] = addresses
r.mu.Unlock() r.mu.Unlock()
} }
// Address implements model.Resolver.Address.
func (r *cacheResolver) Address() string {
return r.resolver.Address()
}
// Network implements model.Resolver.Network.
func (r *cacheResolver) Network() string {
return r.resolver.Network()
}
// CloseIdleConnections implements model.Resolver.CloseIdleConnections.
func (r *cacheResolver) CloseIdleConnections() {
r.resolver.CloseIdleConnections()
}
// LookupHTTPS implements model.Resolver.LookupHTTPS.
func (r *cacheResolver) LookupHTTPS(ctx context.Context, domain string) (*model.HTTPSSvc, error) {
return nil, netxlite.ErrNoDNSTransport
}
// LookupNS implements model.Resolver.LookupNS.
func (r *cacheResolver) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) {
return nil, netxlite.ErrNoDNSTransport
}

View File

@ -5,81 +5,202 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/model/mocks" "github.com/ooni/probe-cli/v3/internal/model/mocks"
"github.com/ooni/probe-cli/v3/internal/netxlite"
) )
func TestCacheResolverFailure(t *testing.T) { func TestMaybeWrapWithCachingResolver(t *testing.T) {
expected := errors.New("mocked error") t.Run("with enable equal to true", func(t *testing.T) {
r := &mocks.Resolver{ underlying := &mocks.Resolver{}
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) { reso := MaybeWrapWithCachingResolver(true, underlying)
return nil, expected cachereso := reso.(*cacheResolver)
}, if cachereso.resolver != underlying {
} t.Fatal("did not wrap correctly")
cache := &CacheResolver{Resolver: r} }
addrs, err := cache.LookupHost(context.Background(), "www.google.com") })
if !errors.Is(err, expected) {
t.Fatal("not the error we expected") t.Run("with enable equal to false", func(t *testing.T) {
} underlying := &mocks.Resolver{}
if addrs != nil { reso := MaybeWrapWithCachingResolver(false, underlying)
t.Fatal("expected nil addrs here") if reso != underlying {
} t.Fatal("unexpected result")
if cache.Get("www.google.com") != nil { }
t.Fatal("expected empty cache here") })
}
} }
func TestCacheResolverHitSuccess(t *testing.T) { func TestMaybeWrapWithStaticDNSCache(t *testing.T) {
expected := errors.New("mocked error") t.Run("when the cache is not empty", func(t *testing.T) {
r := &mocks.Resolver{ cachedDomain := "dns.google"
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) { expectedEntry := []string{"8.8.8.8", "8.8.4.4"}
return nil, expected underlyingCache := make(map[string][]string)
}, underlyingCache[cachedDomain] = expectedEntry
} underlyingReso := &mocks.Resolver{}
cache := &CacheResolver{Resolver: r} reso := MaybeWrapWithStaticDNSCache(underlyingCache, underlyingReso)
cache.Set("dns.google.com", []string{"8.8.8.8"}) cachereso := reso.(*cacheResolver)
addrs, err := cache.LookupHost(context.Background(), "dns.google.com") if diff := cmp.Diff(cachereso.cache, underlyingCache); diff != "" {
if err != nil { t.Fatal(diff)
t.Fatal(err) }
} if cachereso.resolver != underlyingReso {
if len(addrs) != 1 || addrs[0] != "8.8.8.8" { t.Fatal("unexpected underlying resolver")
t.Fatal("not the result we expected") }
} })
t.Run("when the cache is empty", func(t *testing.T) {
underlyingCache := make(map[string][]string)
underlyingReso := &mocks.Resolver{}
reso := MaybeWrapWithStaticDNSCache(underlyingCache, underlyingReso)
if reso != underlyingReso {
t.Fatal("unexpected result")
}
})
t.Run("when the cache is nil", func(t *testing.T) {
var underlyingCache map[string][]string
underlyingReso := &mocks.Resolver{}
reso := MaybeWrapWithStaticDNSCache(underlyingCache, underlyingReso)
if reso != underlyingReso {
t.Fatal("unexpected result")
}
})
} }
func TestCacheResolverMissSuccess(t *testing.T) { func TestCacheResolver(t *testing.T) {
r := &mocks.Resolver{ t.Run("LookupHost", func(t *testing.T) {
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) { t.Run("cache miss and failure", func(t *testing.T) {
return []string{"8.8.8.8"}, nil expected := errors.New("mocked error")
}, r := &mocks.Resolver{
} MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
cache := &CacheResolver{Resolver: r} return nil, expected
addrs, err := cache.LookupHost(context.Background(), "dns.google.com") },
if err != nil { }
t.Fatal(err) cache := &cacheResolver{resolver: r}
} addrs, err := cache.LookupHost(context.Background(), "www.google.com")
if len(addrs) != 1 || addrs[0] != "8.8.8.8" { if !errors.Is(err, expected) {
t.Fatal("not the result we expected") t.Fatal("not the error we expected")
} }
if cache.Get("dns.google.com")[0] != "8.8.8.8" { if addrs != nil {
t.Fatal("expected full cache here") t.Fatal("expected nil addrs here")
} }
} if cache.get("www.google.com") != nil {
t.Fatal("expected empty cache here")
}
})
func TestCacheResolverReadonlySuccess(t *testing.T) { t.Run("cache hit", func(t *testing.T) {
r := &mocks.Resolver{ expected := errors.New("mocked error")
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) { r := &mocks.Resolver{
return []string{"8.8.8.8"}, nil MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
}, return nil, expected
} },
cache := &CacheResolver{Resolver: r, ReadOnly: true} }
addrs, err := cache.LookupHost(context.Background(), "dns.google.com") cache := &cacheResolver{resolver: r}
if err != nil { cache.set("dns.google.com", []string{"8.8.8.8"})
t.Fatal(err) addrs, err := cache.LookupHost(context.Background(), "dns.google.com")
} if err != nil {
if len(addrs) != 1 || addrs[0] != "8.8.8.8" { t.Fatal(err)
t.Fatal("not the result we expected") }
} if len(addrs) != 1 || addrs[0] != "8.8.8.8" {
if cache.Get("dns.google.com") != nil { t.Fatal("not the result we expected")
t.Fatal("expected empty cache here") }
} })
t.Run("cache miss and success with readwrite cache", func(t *testing.T) {
r := &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return []string{"8.8.8.8"}, nil
},
}
cache := &cacheResolver{resolver: r}
addrs, err := cache.LookupHost(context.Background(), "dns.google.com")
if err != nil {
t.Fatal(err)
}
if len(addrs) != 1 || addrs[0] != "8.8.8.8" {
t.Fatal("not the result we expected")
}
if cache.get("dns.google.com")[0] != "8.8.8.8" {
t.Fatal("expected full cache here")
}
})
t.Run("cache miss and success with readonly cache", func(t *testing.T) {
r := &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return []string{"8.8.8.8"}, nil
},
}
cache := &cacheResolver{resolver: r, readOnly: true}
addrs, err := cache.LookupHost(context.Background(), "dns.google.com")
if err != nil {
t.Fatal(err)
}
if len(addrs) != 1 || addrs[0] != "8.8.8.8" {
t.Fatal("not the result we expected")
}
if cache.get("dns.google.com") != nil {
t.Fatal("expected empty cache here")
}
})
t.Run("Address", func(t *testing.T) {
underlying := &mocks.Resolver{
MockAddress: func() string {
return "x"
},
}
reso := &cacheResolver{resolver: underlying}
if reso.Address() != "x" {
t.Fatal("unexpected result")
}
})
t.Run("Network", func(t *testing.T) {
underlying := &mocks.Resolver{
MockNetwork: func() string {
return "x"
},
}
reso := &cacheResolver{resolver: underlying}
if reso.Network() != "x" {
t.Fatal("unexpected result")
}
})
t.Run("CloseIdleConnections", func(t *testing.T) {
var called bool
underlying := &mocks.Resolver{
MockCloseIdleConnections: func() {
called = true
},
}
reso := &cacheResolver{resolver: underlying}
reso.CloseIdleConnections()
if !called {
t.Fatal("not called")
}
})
t.Run("LookupHTTPS", func(t *testing.T) {
reso := &cacheResolver{}
https, err := reso.LookupHTTPS(context.Background(), "dns.google")
if !errors.Is(err, netxlite.ErrNoDNSTransport) {
t.Fatal("unexpected err", err)
}
if https != nil {
t.Fatal("expected nil")
}
})
t.Run("LookupNS", func(t *testing.T) {
reso := &cacheResolver{}
ns, err := reso.LookupNS(context.Background(), "dns.google")
if !errors.Is(err, netxlite.ErrNoDNSTransport) {
t.Fatal("unexpected err", err)
}
if len(ns) != 0 {
t.Fatal("expected zero length slice")
}
})
})
} }

View File

@ -67,19 +67,9 @@ func NewResolver(config Config) model.Resolver {
model.ValidLoggerOrDefault(config.Logger), model.ValidLoggerOrDefault(config.Logger),
config.BaseResolver, config.BaseResolver,
) )
if config.CacheResolutions { r = MaybeWrapWithCachingResolver(config.CacheResolutions, r)
r = &CacheResolver{Resolver: r} r = MaybeWrapWithStaticDNSCache(config.DNSCache, r)
} r = netxlite.MaybeWrapWithBogonResolver(config.BogonIsError, r)
if config.DNSCache != nil {
cache := &CacheResolver{Resolver: r, ReadOnly: true}
for key, values := range config.DNSCache {
cache.Set(key, values)
}
r = cache
}
if config.BogonIsError {
r = &netxlite.BogonResolver{Resolver: r}
}
return config.Saver.WrapResolver(r) // WAI when config.Saver==nil return config.Saver.WrapResolver(r) // WAI when config.Saver==nil
} }
@ -94,9 +84,7 @@ func NewDialer(config Config) model.Dialer {
config.ReadWriteSaver.NewReadWriteObserver(), config.ReadWriteSaver.NewReadWriteObserver(),
) )
d = netxlite.NewMaybeProxyDialer(d, config.ProxyURL) d = netxlite.NewMaybeProxyDialer(d, config.ProxyURL)
if config.ContextByteCounting { d = bytecounter.MaybeWrapWithContextAwareDialer(config.ContextByteCounting, d)
d = &bytecounter.ContextAwareDialer{Dialer: d}
}
return d return d
} }
@ -143,15 +131,12 @@ func NewHTTPTransport(config Config) model.HTTPTransport {
TLSDialer: config.TLSDialer, TLSDialer: config.TLSDialer,
TLSConfig: config.TLSConfig, TLSConfig: config.TLSConfig,
}) })
if config.ByteCounter != nil { // TODO(bassosimone): I am not super convinced by this code because it
txp = &bytecounter.HTTPTransport{ // seems we're currently counting bytes twice in some cases. I think we
Counter: config.ByteCounter, HTTPTransport: txp} // should review how we're counting bytes and using netx currently.
} txp = config.ByteCounter.MaybeWrapHTTPTransport(txp) // WAI with ByteCounter == nil
if config.Saver != nil { const defaultSnapshotSize = 0 // means: use the default snapsize
txp = &tracex.HTTPTransportSaver{ return config.Saver.MaybeWrapHTTPTransport(txp, defaultSnapshotSize) // WAI with Saver == nil
HTTPTransport: txp, Saver: config.Saver}
}
return txp
} }
// httpTransportInfo contains the constructing function as well as the transport name // httpTransportInfo contains the constructing function as well as the transport name

View File

@ -100,21 +100,6 @@ func TestNewWithDialer(t *testing.T) {
} }
} }
func TestNewWithByteCounter(t *testing.T) {
counter := bytecounter.New()
txp := NewHTTPTransport(Config{
ByteCounter: counter,
})
bctxp, ok := txp.(*bytecounter.HTTPTransport)
if !ok {
t.Fatal("not the transport we expected")
}
if bctxp.Counter != counter {
t.Fatal("not the byte counter we expected")
}
// We are going to trust the underlying transport returned by netxlite
}
func TestNewWithSaver(t *testing.T) { func TestNewWithSaver(t *testing.T) {
saver := new(tracex.Saver) saver := new(tracex.Saver)
txp := NewHTTPTransport(Config{ txp := NewHTTPTransport(Config{

View File

@ -202,7 +202,7 @@ func NewSession(ctx context.Context, config SessionConfig) (*Session, error) {
handshaker := netxlite.NewTLSHandshakerStdlib(sess.logger) handshaker := netxlite.NewTLSHandshakerStdlib(sess.logger)
tlsDialer := netxlite.NewTLSDialer(dialer, handshaker) tlsDialer := netxlite.NewTLSDialer(dialer, handshaker)
txp := netxlite.NewHTTPTransport(sess.logger, dialer, tlsDialer) txp := netxlite.NewHTTPTransport(sess.logger, dialer, tlsDialer)
txp = bytecounter.NewHTTPTransport(txp, sess.byteCounter) txp = bytecounter.WrapHTTPTransport(txp, sess.byteCounter)
sess.httpDefaultTransport = txp sess.httpDefaultTransport = txp
return sess, nil return sess, nil
} }

View File

@ -14,6 +14,16 @@ import (
"github.com/ooni/probe-cli/v3/internal/runtimex" "github.com/ooni/probe-cli/v3/internal/runtimex"
) )
// MaybeWrapWithBogonResolver wraps the given resolver with a BogonResolver
// iff the provided boolean flag is true. Otherwise, this factory just returns
// the provided resolver to the caller without any wrapping.
func MaybeWrapWithBogonResolver(enabled bool, reso model.Resolver) model.Resolver {
if enabled {
reso = &BogonResolver{Resolver: reso}
}
return reso
}
// BogonResolver is a bogon aware resolver. When a bogon is encountered in // BogonResolver is a bogon aware resolver. When a bogon is encountered in
// a reply, this resolver will return ErrDNSBogon. // a reply, this resolver will return ErrDNSBogon.
// //

View File

@ -9,6 +9,25 @@ import (
"github.com/ooni/probe-cli/v3/internal/model/mocks" "github.com/ooni/probe-cli/v3/internal/model/mocks"
) )
func TestMaybeWrapWithBogonResolver(t *testing.T) {
t.Run("with enabled equal to true", func(t *testing.T) {
underlying := &mocks.Resolver{}
reso := MaybeWrapWithBogonResolver(true, underlying)
bogoreso := reso.(*BogonResolver)
if bogoreso.Resolver != underlying {
t.Fatal("did not wrap")
}
})
t.Run("with enabled equal to false", func(t *testing.T) {
underlying := &mocks.Resolver{}
reso := MaybeWrapWithBogonResolver(false, underlying)
if reso != underlying {
t.Fatal("expected unmodified resolver")
}
})
}
func TestBogonResolver(t *testing.T) { func TestBogonResolver(t *testing.T) {
t.Run("LookupHost", func(t *testing.T) { t.Run("LookupHost", func(t *testing.T) {
t.Run("with failure", func(t *testing.T) { t.Run("with failure", func(t *testing.T) {

View File

@ -163,8 +163,8 @@ func (lst *Listener) handleSocksConn(ctx context.Context, socksConn ptxSocksConn
// We _must_ wrap the ptConn. Wrapping the socks conn leads us to // We _must_ wrap the ptConn. Wrapping the socks conn leads us to
// count the sent bytes as received and the received bytes as sent: // count the sent bytes as received and the received bytes as sent:
// bytes flow in the opposite direction there for the socks conn. // bytes flow in the opposite direction there for the socks conn.
ptConn = bytecounter.MaybeWrap(ptConn, lst.SessionByteCounter) ptConn = bytecounter.MaybeWrapConn(ptConn, lst.SessionByteCounter)
ptConn = bytecounter.MaybeWrap(ptConn, lst.ExperimentByteCounter) ptConn = bytecounter.MaybeWrapConn(ptConn, lst.ExperimentByteCounter)
lst.forwardWithContext(ctx, socksConn, ptConn) // transfer ownership lst.forwardWithContext(ctx, socksConn, ptConn) // transfer ownership
return nil // used for testing return nil // used for testing
} }

View File

@ -14,6 +14,20 @@ import (
"github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/netxlite"
) )
// MaybeWrapHTTPTransport wraps the HTTPTransport to save events if this Saver
// is not nil and otherwise just returns the given HTTPTransport. The snapshotSize
// argument is the maximum response body snapshot size to save per response.
func (s *Saver) MaybeWrapHTTPTransport(txp model.HTTPTransport, snapshotSize int64) model.HTTPTransport {
if s != nil {
txp = &HTTPTransportSaver{
HTTPTransport: txp,
Saver: s,
SnapshotSize: snapshotSize,
}
}
return txp
}
// httpCloneRequestHeaders returns a clone of the headers where we have // httpCloneRequestHeaders returns a clone of the headers where we have
// also set the host header, which normally is not set by // also set the host header, which normally is not set by
// golang until it serializes the request itself. // golang until it serializes the request itself.

View File

@ -15,6 +15,32 @@ import (
"github.com/ooni/probe-cli/v3/internal/netxlite/filtering" "github.com/ooni/probe-cli/v3/internal/netxlite/filtering"
) )
func TestMaybeWrapHTTPTransport(t *testing.T) {
const snapshotSize = 1024
t.Run("with non-nil saver", func(t *testing.T) {
saver := &Saver{}
underlying := &mocks.HTTPTransport{}
txp := saver.MaybeWrapHTTPTransport(underlying, snapshotSize)
realTxp := txp.(*HTTPTransportSaver)
if realTxp.HTTPTransport != underlying {
t.Fatal("unexpected result")
}
if realTxp.SnapshotSize != snapshotSize {
t.Fatal("did not set snapshotSize correctly")
}
})
t.Run("with nil saver", func(t *testing.T) {
var saver *Saver
underlying := &mocks.HTTPTransport{}
txp := saver.MaybeWrapHTTPTransport(underlying, snapshotSize)
if txp != underlying {
t.Fatal("unexpected result")
}
})
}
func TestHTTPTransportSaver(t *testing.T) { func TestHTTPTransportSaver(t *testing.T) {
t.Run("CloseIdleConnections", func(t *testing.T) { t.Run("CloseIdleConnections", func(t *testing.T) {