Skip to content

Commit

Permalink
feat: introduce model.MeasuringNetwork
Browse files Browse the repository at this point in the history
This diff introduces the model.MeasuringNetwork interface that
defines the factories required for implementing experiments.

By defining this interface, we can make measuring code depend on it
rather than on the netxlite package.

While there, create a specific mocks file for the UDPListener.

Reference issue: ooni/probe#2531
  • Loading branch information
bassosimone committed Sep 12, 2023
1 parent 3fcd5b0 commit a7dbe9a
Show file tree
Hide file tree
Showing 14 changed files with 306 additions and 110 deletions.
67 changes: 67 additions & 0 deletions internal/mocks/measuringnetwork.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package mocks

import (
"github.com/ooni/probe-cli/v3/internal/model"
tls "gitlab.com/yawning/utls.git"
)

// MeasuringNetwork allows mocking [model.MeasuringNetwork].
type MeasuringNetwork struct {
MockNewDialerWithResolver func(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer

MockNewParallelDNSOverHTTPSResolver func(logger model.DebugLogger, URL string) model.Resolver

MockNewParallelUDPResolver func(logger model.DebugLogger, dialer model.Dialer, address string) model.Resolver

MockNewQUICDialerWithResolver func(listener model.UDPListener, logger model.DebugLogger, resolver model.Resolver, w ...model.QUICDialerWrapper) model.QUICDialer

MockNewStdlibResolver func(logger model.DebugLogger) model.Resolver

MockNewTLSHandshakerStdlib func(logger model.DebugLogger) model.TLSHandshaker

MockNewTLSHandshakerUTLS func(logger model.DebugLogger, id *tls.ClientHelloID) model.TLSHandshaker

MockNewUDPListener func() model.UDPListener
}

var _ model.MeasuringNetwork = &MeasuringNetwork{}

// NewDialerWithResolver implements model.MeasuringNetwork.
func (mn *MeasuringNetwork) NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer {
return mn.MockNewDialerWithResolver(dl, r, w...)
}

// NewParallelDNSOverHTTPSResolver implements model.MeasuringNetwork.
func (mn *MeasuringNetwork) NewParallelDNSOverHTTPSResolver(logger model.DebugLogger, URL string) model.Resolver {
return mn.MockNewParallelDNSOverHTTPSResolver(logger, URL)
}

// NewParallelUDPResolver implements model.MeasuringNetwork.
func (mn *MeasuringNetwork) NewParallelUDPResolver(logger model.DebugLogger, dialer model.Dialer, address string) model.Resolver {
return mn.MockNewParallelUDPResolver(logger, dialer, address)
}

// NewQUICDialerWithResolver implements model.MeasuringNetwork.
func (mn *MeasuringNetwork) NewQUICDialerWithResolver(listener model.UDPListener, logger model.DebugLogger, resolver model.Resolver, w ...model.QUICDialerWrapper) model.QUICDialer {
return mn.MockNewQUICDialerWithResolver(listener, logger, resolver, w...)
}

// NewStdlibResolver implements model.MeasuringNetwork.
func (mn *MeasuringNetwork) NewStdlibResolver(logger model.DebugLogger) model.Resolver {
return mn.MockNewStdlibResolver(logger)
}

// NewTLSHandshakerStdlib implements model.MeasuringNetwork.
func (mn *MeasuringNetwork) NewTLSHandshakerStdlib(logger model.DebugLogger) model.TLSHandshaker {
return mn.MockNewTLSHandshakerStdlib(logger)
}

// NewTLSHandshakerUTLS implements model.MeasuringNetwork.
func (mn *MeasuringNetwork) NewTLSHandshakerUTLS(logger model.DebugLogger, id *tls.ClientHelloID) model.TLSHandshaker {
return mn.MockNewTLSHandshakerUTLS(logger, id)
}

// NewUDPListener implements model.MeasuringNetwork.
func (mn *MeasuringNetwork) NewUDPListener() model.UDPListener {
return mn.MockNewUDPListener()
}
114 changes: 114 additions & 0 deletions internal/mocks/measuringnetwork_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package mocks

import (
"testing"

"github.com/ooni/probe-cli/v3/internal/model"
tls "gitlab.com/yawning/utls.git"
)

func TestMeasuringN(t *testing.T) {
t.Run("MockNewDialerWithResolver", func(t *testing.T) {
expected := &Dialer{}
mn := &MeasuringNetwork{
MockNewDialerWithResolver: func(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer {
return expected
},
}
got := mn.NewDialerWithResolver(nil, nil)
if expected != got {
t.Fatal("unexpected result")
}
})

t.Run("MockNewParallelDNSOverHTTPSResolver", func(t *testing.T) {
expected := &Resolver{}
mn := &MeasuringNetwork{
MockNewParallelDNSOverHTTPSResolver: func(logger model.DebugLogger, URL string) model.Resolver {
return expected
},
}
got := mn.NewParallelDNSOverHTTPSResolver(nil, "")
if expected != got {
t.Fatal("unexpected result")
}
})

t.Run("MockNewParallelUDPResolver", func(t *testing.T) {
expected := &Resolver{}
mn := &MeasuringNetwork{
MockNewParallelUDPResolver: func(logger model.DebugLogger, dialer model.Dialer, address string) model.Resolver {
return expected
},
}
got := mn.NewParallelUDPResolver(nil, nil, "")
if expected != got {
t.Fatal("unexpected result")
}
})

t.Run("MockNewQUICDialerWithResolver", func(t *testing.T) {
expected := &QUICDialer{}
mn := &MeasuringNetwork{
MockNewQUICDialerWithResolver: func(listener model.UDPListener, logger model.DebugLogger, resolver model.Resolver, w ...model.QUICDialerWrapper) model.QUICDialer {
return expected
},
}
got := mn.NewQUICDialerWithResolver(nil, nil, nil)
if expected != got {
t.Fatal("unexpected result")
}
})

t.Run("MockNewStdlibResolver", func(t *testing.T) {
expected := &Resolver{}
mn := &MeasuringNetwork{
MockNewStdlibResolver: func(logger model.DebugLogger) model.Resolver {
return expected
},
}
got := mn.NewStdlibResolver(nil)
if expected != got {
t.Fatal("unexpected result")
}
})

t.Run("MockNewTLSHandshakerStdlib", func(t *testing.T) {
expected := &TLSHandshaker{}
mn := &MeasuringNetwork{
MockNewTLSHandshakerStdlib: func(logger model.DebugLogger) model.TLSHandshaker {
return expected
},
}
got := mn.NewTLSHandshakerStdlib(nil)
if expected != got {
t.Fatal("unexpected result")
}
})

t.Run("MockNewTLSHandshakerUTLS", func(t *testing.T) {
expected := &TLSHandshaker{}
mn := &MeasuringNetwork{
MockNewTLSHandshakerUTLS: func(logger model.DebugLogger, id *tls.ClientHelloID) model.TLSHandshaker {
return expected
},
}
got := mn.NewTLSHandshakerUTLS(nil, nil)
if expected != got {
t.Fatal("unexpected result")
}
})

t.Run("MockNewUDPListener", func(t *testing.T) {
expected := &UDPListener{}
mn := &MeasuringNetwork{
MockNewUDPListener: func() model.UDPListener {
return expected
},
}
got := mn.NewUDPListener()
if expected != got {
t.Fatal("unexpected result")
}
})
}
10 changes: 0 additions & 10 deletions internal/mocks/quic.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,6 @@ import (
"github.com/quic-go/quic-go"
)

// UDPListener is a mockable netxlite.UDPListener.
type UDPListener struct {
MockListen func(addr *net.UDPAddr) (model.UDPLikeConn, error)
}

// Listen calls MockListen.
func (ql *UDPListener) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) {
return ql.MockListen(addr)
}

// QUICDialer is a mockable netxlite.QUICDialer.
type QUICDialer struct {
// MockDialContext allows mocking DialContext.
Expand Down
19 changes: 0 additions & 19 deletions internal/mocks/quic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,9 @@ import (
"time"

"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/quic-go/quic-go"
)

func TestUDPListenerListen(t *testing.T) {
t.Run("Listen", func(t *testing.T) {
expected := errors.New("mocked error")
ql := &UDPListener{
MockListen: func(addr *net.UDPAddr) (model.UDPLikeConn, error) {
return nil, expected
},
}
pconn, err := ql.Listen(&net.UDPAddr{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", expected)
}
if pconn != nil {
t.Fatal("expected nil conn here")
}
})
}

func TestQUICDialer(t *testing.T) {
t.Run("DialContext", func(t *testing.T) {
expected := errors.New("mocked error")
Expand Down
17 changes: 17 additions & 0 deletions internal/mocks/udplistener.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package mocks

import (
"net"

"github.com/ooni/probe-cli/v3/internal/model"
)

// UDPListener is a mockable netxlite.UDPListener.
type UDPListener struct {
MockListen func(addr *net.UDPAddr) (model.UDPLikeConn, error)
}

// Listen calls MockListen.
func (ql *UDPListener) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) {
return ql.MockListen(addr)
}
27 changes: 27 additions & 0 deletions internal/mocks/udplistener_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package mocks

import (
"errors"
"net"
"testing"

"github.com/ooni/probe-cli/v3/internal/model"
)

func TestUDPListener(t *testing.T) {
t.Run("Listen", func(t *testing.T) {
expected := errors.New("mocked error")
ql := &UDPListener{
MockListen: func(addr *net.UDPAddr) (model.UDPLikeConn, error) {
return nil, expected
},
}
pconn, err := ql.Listen(&net.UDPAddr{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", expected)
}
if pconn != nil {
t.Fatal("expected nil conn here")
}
})
}
73 changes: 68 additions & 5 deletions internal/model/netx.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import (
"syscall"
"time"

oohttp "github.com/ooni/oohttp"
"github.com/quic-go/quic-go"
utls "gitlab.com/yawning/utls.git"
)

// DNSResponse is a parsed DNS response ready for further processing.
Expand Down Expand Up @@ -175,10 +177,55 @@ type HTTPSSvc struct {
IPv6 []string
}

// UDPListener listens for connections over UDP, e.g. QUIC.
type UDPListener interface {
// Listen creates a new listening UDPLikeConn.
Listen(addr *net.UDPAddr) (UDPLikeConn, error)
// MeasuringNetwork defines the constructors required for implementing OONI experiments. All
// these constructors MUST guarantee proper error wrapping to map Go errors to OONI errors
// as documented by the [netxlite] package. The [*netxlite.Netx] type is currently the default
// implementation of this interface. This interface SHOULD always be implemented in terms of
// an [UnderlyingNetwork] that allows to switch between the host network and [netemx].
type MeasuringNetwork interface {
// NewDialerWithResolver creates a [Dialer] with error wrapping.
//
// This dialer will try to connect to each of the resolved IP address
// sequentially. In case of failure, such a resolver will return the first
// error that occurred. This implementation strategy is a QUIRK that is
// documented at TODO(https://github.com/ooni/probe/issues/1779).
NewDialerWithResolver(dl DebugLogger, r Resolver, w ...DialerWrapper) Dialer

// NewParallelDNSOverHTTPSResolver creates a new DNS-over-HTTPS resolver with error wrapping.
NewParallelDNSOverHTTPSResolver(logger DebugLogger, URL string) Resolver

// NewParallelUDPResolver creates a new Resolver using DNS-over-UDP
// that performs parallel A/AAAA lookups during LookupHost.
//
// The address argument is the UDP endpoint address (e.g., 1.1.1.1:53, [::1]:53).
NewParallelUDPResolver(logger DebugLogger, dialer Dialer, address string) Resolver

// NewQUICDialerWithResolver creates a QUICDialer with error wrapping.
//
// Unlike the dialer returned by NewDialerWithResolver, this dialer MAY attempt
// happy eyeballs, perform parallel dial attempts, and return an error
// that aggregates all the errors that occurred.
NewQUICDialerWithResolver(
listener UDPListener, logger DebugLogger, resolver Resolver, w ...QUICDialerWrapper) QUICDialer

// NewStdlibResolver creates a new Resolver with error wrapping using
// getaddrinfo or &net.Resolver{} depending on `-tags netgo`.
NewStdlibResolver(logger DebugLogger) Resolver

// NewTLSHandshakerStdlib creates a new TLSHandshaker with error wrapping
// that is using the go standard library to manage TLS.
NewTLSHandshakerStdlib(logger DebugLogger) TLSHandshaker

// NewTLSHandshakerUTLS creates a new TLS handshaker using
// gitlab.com/yawning/utls for TLS that implements error wrapping.
//
// The id is the address of something like utls.HelloFirefox_55.
//
// Passing a nil `id` will make this function panic.
NewTLSHandshakerUTLS(logger DebugLogger, id *utls.ClientHelloID) TLSHandshaker

// NewUDPListener creates a new UDPListener with error wrapping.
NewUDPListener() UDPListener
}

// QUICDialerWrapper is a type that takes in input a QUICDialer
Expand Down Expand Up @@ -251,6 +298,16 @@ type Resolver interface {
LookupNS(ctx context.Context, domain string) ([]*net.NS, error)
}

// TLSConn is the type of connection that oohttp expects from
// any library that implements TLS functionality. By using this
// kind of TLSConn we're able to use both the standard library
// and gitlab.com/yawning/utls.git to perform TLS operations. Note
// that the stdlib's tls.Conn implements this interface.
type TLSConn = oohttp.TLSConn

// Ensures that a [*tls.Conn] implements the [TLSConn] interface.
var _ TLSConn = &tls.Conn{}

// TLSDialer is a Dialer dialing TLS connections.
type TLSDialer interface {
// CloseIdleConnections closes idle connections, if any.
Expand Down Expand Up @@ -476,8 +533,14 @@ type UDPLikeConn interface {
SyscallConn() (syscall.RawConn, error)
}

// UDPListener listens for connections over UDP, e.g. QUIC.
type UDPListener interface {
// Listen creates a new listening UDPLikeConn.
Listen(addr *net.UDPAddr) (UDPLikeConn, error)
}

// UnderlyingNetwork implements the underlying network APIs on
// top of which we implement network extensions.
// top of which we implement network extensions such as [MeasuringNetwork].
type UnderlyingNetwork interface {
// DefaultCertPool returns the underlying cert pool used by the
// network extensions library. You MUST NOT use this function to
Expand Down
5 changes: 2 additions & 3 deletions internal/netxlite/dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ func NewDialerWithStdlibResolver(dl model.DebugLogger) model.Dialer {
return NewDialerWithResolver(dl, reso)
}

// NewDialerWithResolver is equivalent to calling WrapDialer with a dialer using the
// the [*Netx] UnderlyingNetwork for dialing new connections.
// NewDialerWithResolver implements [model.MeasuringNetwork].
func (netx *Netx) NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer {
return WrapDialer(dl, r, &dialerSystem{provider: netx.maybeCustomUnderlyingNetwork()}, w...)
}
Expand Down Expand Up @@ -109,7 +108,7 @@ func NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.Di
// When the resolver is &NullResolver{} any attempt to perform DNS resolutions
// in the dialer at index N+2 will fail with ErrNoResolver.
//
// Otherwise, the dialer at index N+2 will try each resolver IP address
// Otherwise, the dialer at index N+2 will try each resolved IP address
// sequentially. In case of failure, such a resolver will return the first
// error that occurred. This implementation strategy is a QUIRK that is
// documented at TODO(https://github.com/ooni/probe/issues/1779).
Expand Down
Loading

0 comments on commit a7dbe9a

Please sign in to comment.