Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions internal/model/mocks/underlyingnetwork.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package mocks

import (
"context"
"net"
"time"

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

// UnderlyingNetwork allows mocking model.UnderlyingNetwork.
type UnderlyingNetwork struct {
MockDialContext func(ctx context.Context, timeout time.Duration, network, address string) (net.Conn, error)

MockListenUDP func(network string, addr *net.UDPAddr) (model.UDPLikeConn, error)

MockGetaddrinfoLookupANY func(ctx context.Context, domain string) ([]string, string, error)

MockGetaddrinfoResolverNetwork func() string
}

var _ model.UnderlyingNetwork = &UnderlyingNetwork{}

func (un *UnderlyingNetwork) DialContext(ctx context.Context, timeout time.Duration, network, address string) (net.Conn, error) {
return un.MockDialContext(ctx, timeout, network, address)
}

func (un *UnderlyingNetwork) ListenUDP(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) {
return un.MockListenUDP(network, addr)
}

func (un *UnderlyingNetwork) GetaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error) {
return un.MockGetaddrinfoLookupANY(ctx, domain)
}

func (un *UnderlyingNetwork) GetaddrinfoResolverNetwork() string {
return un.MockGetaddrinfoResolverNetwork()
}
79 changes: 79 additions & 0 deletions internal/model/mocks/underlyingnetwork_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package mocks

import (
"context"
"errors"
"net"
"testing"
"time"

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

func TestUnderlyingNetwork(t *testing.T) {
t.Run("DialContext", func(t *testing.T) {
expect := errors.New("mocked error")
un := &UnderlyingNetwork{
MockDialContext: func(ctx context.Context, timeout time.Duration, network, address string) (net.Conn, error) {
return nil, expect
},
}
ctx := context.Background()
conn, err := un.DialContext(ctx, time.Second, "tcp", "1.1.1.1:443")
if !errors.Is(err, expect) {
t.Fatal("unexpected err", err)
}
if conn != nil {
t.Fatal("expected nil conn")
}
})

t.Run("ListenUDP", func(t *testing.T) {
expect := errors.New("mocked error")
un := &UnderlyingNetwork{
MockListenUDP: func(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) {
return nil, expect
},
}
pconn, err := un.ListenUDP("udp", &net.UDPAddr{})
if !errors.Is(err, expect) {
t.Fatal("unexpected err", err)
}
if pconn != nil {
t.Fatal("expected nil conn")
}
})

t.Run("GetaddrinfoLookupANY", func(t *testing.T) {
expect := errors.New("mocked error")
un := &UnderlyingNetwork{
MockGetaddrinfoLookupANY: func(ctx context.Context, domain string) ([]string, string, error) {
return nil, "", expect
},
}
ctx := context.Background()
addrs, cname, err := un.GetaddrinfoLookupANY(ctx, "dns.google")
if !errors.Is(err, expect) {
t.Fatal("unexpected err", err)
}
if len(addrs) != 0 {
t.Fatal("expected zero length addrs")
}
if cname != "" {
t.Fatal("expected empty name")
}
})

t.Run("GetaddrinfoResolverNetwork", func(t *testing.T) {
expect := "antani"
un := &UnderlyingNetwork{
MockGetaddrinfoResolverNetwork: func() string {
return expect
},
}
got := un.GetaddrinfoResolverNetwork()
if got != expect {
t.Fatal("unexpected resolver network")
}
})
}
18 changes: 18 additions & 0 deletions internal/model/netx.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,3 +480,21 @@ type UDPLikeConn interface {
// which is also instrumental to setting the read buffer.
SyscallConn() (syscall.RawConn, error)
}

// UnderlyingNetwork implements the underlying network APIs on
// top of which we implement network extensions.
type UnderlyingNetwork interface {
// DialContext is equivalent to net.Dialer.DialContext except that
// there is also an explicit timeout for dialing.
DialContext(ctx context.Context, timeout time.Duration, network, address string) (net.Conn, error)

// ListenUDP is equivalent to net.ListenUDP.
ListenUDP(network string, addr *net.UDPAddr) (UDPLikeConn, error)

// GetaddrinfoLookupANY is like net.Resolver.LookupHost except that it
// also returns to the caller the CNAME when it is available.
GetaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error)

// GetaddrinfoResolverNetwork returns the resolver network.
GetaddrinfoResolverNetwork() string
}
87 changes: 43 additions & 44 deletions internal/netxlite/dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.Di
// When possible use NewDialerWithResolver or NewDialerWithoutResolver
// instead of using this rather low-level function.
//
// Arguments
// # Arguments
//
// 1. logger is used to emit debug messages (MUST NOT be nil);
//
Expand All @@ -47,58 +47,57 @@ func NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.Di
// modify the behavior of the returned dialer (see below). Please note
// that this function will just ignore any nil wrapper.
//
// Return value
// # Return value
//
// The returned dialer is an opaque type consisting of the composition of
// several simple dialers. The following pseudo code illustrates the general
// behavior of the returned composed dialer:
//
// addrs, err := dnslookup()
// if err != nil {
// return nil, err
// }
// errors := []error{}
// for _, a := range addrs {
// conn, err := tcpconnect(a)
// if err != nil {
// errors = append(errors, err)
// continue
// }
// return conn, nil
// }
// return nil, errors[0]
//
// addrs, err := dnslookup()
// if err != nil {
// return nil, err
// }
// errors := []error{}
// for _, a := range addrs {
// conn, err := tcpconnect(a)
// if err != nil {
// errors = append(errors, err)
// continue
// }
// return conn, nil
// }
// return nil, errors[0]
//
// The following table describes the structure of the returned dialer:
//
// +-------+-----------------+------------------------------------------+
// | Index | Name | Description |
// +-------+-----------------+------------------------------------------+
// | 0 | base | the baseDialer argument |
// +-------+-----------------+------------------------------------------+
// | 1 | errWrapper | wraps Go errors to be consistent with |
// | | | OONI df-007-errors spec |
// +-------+-----------------+------------------------------------------+
// | 2 | ??? | if there are wrappers, result of calling |
// | | | the first one on the errWrapper dialer |
// +-------+-----------------+------------------------------------------+
// | ... | ... | ... |
// +-------+-----------------+------------------------------------------+
// | N | ??? | if there are wrappers, result of calling |
// | | | the last one on the N-1 dialer |
// +-------+-----------------+------------------------------------------+
// | N+1 | logger (inner) | logs TCP connect operations |
// +-------+-----------------+------------------------------------------+
// | N+2 | resolver | DNS lookup and try connect each IP in |
// | | | sequence until one of them succeeds |
// +-------+-----------------+------------------------------------------+
// | N+3 | logger (outer) | logs the overall dial operation |
// +-------+-----------------+------------------------------------------+
// +-------+-----------------+------------------------------------------+
// | Index | Name | Description |
// +-------+-----------------+------------------------------------------+
// | 0 | base | the baseDialer argument |
// +-------+-----------------+------------------------------------------+
// | 1 | errWrapper | wraps Go errors to be consistent with |
// | | | OONI df-007-errors spec |
// +-------+-----------------+------------------------------------------+
// | 2 | ??? | if there are wrappers, result of calling |
// | | | the first one on the errWrapper dialer |
// +-------+-----------------+------------------------------------------+
// | ... | ... | ... |
// +-------+-----------------+------------------------------------------+
// | N | ??? | if there are wrappers, result of calling |
// | | | the last one on the N-1 dialer |
// +-------+-----------------+------------------------------------------+
// | N+1 | logger (inner) | logs TCP connect operations |
// +-------+-----------------+------------------------------------------+
// | N+2 | resolver | DNS lookup and try connect each IP in |
// | | | sequence until one of them succeeds |
// +-------+-----------------+------------------------------------------+
// | N+3 | logger (outer) | logs the overall dial operation |
// +-------+-----------------+------------------------------------------+
//
// The list of wrappers allows to insert modified dialers in the correct
// place for observing and saving I/O events (connect, read, etc.).
//
// Remarks
// # Remarks
//
// When the resolver is &NullResolver{} any attempt to perform DNS resolutions
// in the dialer at index N+2 will fail with ErrNoResolver.
Expand Down Expand Up @@ -155,16 +154,16 @@ var _ model.Dialer = &DialerSystem{}

const dialerDefaultTimeout = 15 * time.Second

func (d *DialerSystem) newUnderlyingDialer() model.SimpleDialer {
func (d *DialerSystem) configuredTimeout() time.Duration {
t := d.timeout
if t <= 0 {
t = dialerDefaultTimeout
}
return &net.Dialer{Timeout: t}
return t
}

func (d *DialerSystem) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return d.newUnderlyingDialer().DialContext(ctx, network, address)
return TProxy.DialContext(ctx, d.configuredTimeout(), network, address)
}

func (d *DialerSystem) CloseIdleConnections() {
Expand Down
8 changes: 4 additions & 4 deletions internal/netxlite/dialer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,17 @@ func TestNewDialer(t *testing.T) {
func TestDialerSystem(t *testing.T) {
t.Run("has a default timeout", func(t *testing.T) {
d := &DialerSystem{}
ud := d.newUnderlyingDialer()
if ud.(*net.Dialer).Timeout != dialerDefaultTimeout {
timeout := d.configuredTimeout()
if timeout != dialerDefaultTimeout {
t.Fatal("unexpected default timeout")
}
})

t.Run("we can change the timeout for testing", func(t *testing.T) {
const smaller = 1 * time.Second
d := &DialerSystem{timeout: smaller}
ud := d.newUnderlyingDialer()
if ud.(*net.Dialer).Timeout != smaller {
timeout := d.configuredTimeout()
if timeout != smaller {
t.Fatal("unexpected timeout")
}
})
Expand Down
4 changes: 2 additions & 2 deletions internal/netxlite/dnsovergetaddrinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,15 @@ func (txp *dnsOverGetaddrinfoTransport) lookupfn() func(ctx context.Context, dom
if txp.testableLookupANY != nil {
return txp.testableLookupANY
}
return getaddrinfoLookupANY
return TProxy.GetaddrinfoLookupANY
}

func (txp *dnsOverGetaddrinfoTransport) RequiresPadding() bool {
return false
}

func (txp *dnsOverGetaddrinfoTransport) Network() string {
return getaddrinfoResolverNetwork()
return TProxy.GetaddrinfoResolverNetwork()
}

func (txp *dnsOverGetaddrinfoTransport) Address() string {
Expand Down
20 changes: 16 additions & 4 deletions internal/netxlite/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
// You should consider checking the tutorial explaining how to use this package
// for network measurements: https://github.com/ooni/probe-cli/tree/master/internal/tutorial/netxlite.
//
// Naming and history
// # Naming and history
//
// Previous versions of this package were called netx. Compared to such
// versions this package is lightweight because it does not contain code
// to perform the measurements, hence its name.
//
// Design
// # Design
//
// We want to potentially be able to observe each low-level operation
// separately, even though this is not done by this package. This is
Expand All @@ -41,7 +41,19 @@
// See also the design document at docs/design/dd-003-step-by-step.md,
// which provides an overview of netxlite's main concerns.
//
// Operations
// To implement integration testing, we support hijacking the core network
// primitives used by this package, that is:
//
// 1. connecting a new TCP/UDP connection;
//
// 2. creating listening UDP sockets;
//
// 3. resolving domain names with getaddrinfo.
//
// By overriding the TProxy variable, you can control these operations and route
// traffic to, e.g., a wireguard peer where you implement censorship.
//
// # Operations
//
// This package implements the following operations:
//
Expand All @@ -62,7 +74,7 @@
// Operations 1, 2, 3, and 4 are used when we perform measurements,
// while 5 and 6 are mostly used when speaking with our backend.
//
// Getaddrinfo usage
// # Getaddrinfo usage
//
// When compiled with CGO_ENABLED=1, this package will link with libc
// and call getaddrinfo directly. While this design choice means we will
Expand Down
2 changes: 1 addition & 1 deletion internal/netxlite/quic.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var _ model.QUICListener = &quicListenerStdlib{}

// Listen implements QUICListener.Listen.
func (qls *quicListenerStdlib) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) {
return net.ListenUDP("udp", addr)
return TProxy.ListenUDP("udp", addr)
}

// NewQUICDialerWithResolver is the WrapDialer equivalent for QUIC where
Expand Down
Loading