ooni-probe-cli/internal/measurexlite/trace_test.go
2022-09-07 15:19:53 +02:00

598 lines
16 KiB
Go

package measurexlite
import (
"context"
"crypto/tls"
"errors"
"net"
"reflect"
"syscall"
"testing"
"time"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/model/mocks"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/testingx"
utls "gitlab.com/yawning/utls.git"
)
func TestNewTrace(t *testing.T) {
t.Run("NewTrace correctly constructs a trace", func(t *testing.T) {
const index = 17
zeroTime := time.Now()
trace := NewTrace(index, zeroTime)
t.Run("Index", func(t *testing.T) {
if trace.Index != index {
t.Fatal("invalid index")
}
})
t.Run("NetworkEvent has the expected buffer size", func(t *testing.T) {
ff := &testingx.FakeFiller{}
var idx int
Loop:
for {
ev := &model.ArchivalNetworkEvent{}
ff.Fill(ev)
select {
case trace.networkEvent <- ev:
idx++
default:
break Loop
}
}
if idx != NetworkEventBufferSize {
t.Fatal("invalid NetworkEvent channel buffer size")
}
})
t.Run("NewStdlibResolverFn is nil", func(t *testing.T) {
if trace.NewStdlibResolverFn != nil {
t.Fatal("expected nil NewStdlibResolverFn")
}
})
t.Run("NewParallelUDPResolverFn is nil", func(t *testing.T) {
if trace.NewParallelUDPResolverFn != nil {
t.Fatal("expected nil NewParallelUDPResolverFn")
}
})
t.Run("NewParallelDNSOverHTTPSResolverFn is nil", func(t *testing.T) {
if trace.NewParallelDNSOverHTTPSResolverFn != nil {
t.Fatal("expected nil NewParallelDNSOverHTTPSResolverFn")
}
})
t.Run("NewDialerWithoutResolverFn is nil", func(t *testing.T) {
if trace.NewDialerWithoutResolverFn != nil {
t.Fatal("expected nil NewDialerWithoutResolverFn")
}
})
t.Run("NewTLSHandshakerStdlibFn is nil", func(t *testing.T) {
if trace.NewTLSHandshakerStdlibFn != nil {
t.Fatal("expected nil NewTLSHandshakerStdlibFn")
}
})
t.Run("newTLShandshakerUTLSFn is nil", func(t *testing.T) {
if trace.NewTLSHandshakerUTLSFn != nil {
t.Fatal("expected nil NewTLSHandshakerUTLSfn")
}
})
t.Run("NewQUICDialerWithoutResolverFn is nil", func(t *testing.T) {
if trace.NewQUICDialerWithoutResolverFn != nil {
t.Fatal("expected nil NewQUICDialerQithoutResolverFn")
}
})
t.Run("dnsLookup has the expected buffer size", func(t *testing.T) {
ff := &testingx.FakeFiller{}
var idx int
Loop:
for {
ev := &model.ArchivalDNSLookupResult{}
ff.Fill(ev)
select {
case trace.dnsLookup <- ev:
idx++
default:
break Loop
}
}
if idx != DNSLookupBufferSize {
t.Fatal("invalid dnsLookup channel buffer size")
}
})
t.Run("delayedDNSResponse has the expected buffer size", func(t *testing.T) {
ff := &testingx.FakeFiller{}
var idx int
Loop:
for {
ev := &model.ArchivalDNSLookupResult{}
ff.Fill(ev)
select {
case trace.delayedDNSResponse <- ev:
idx++
default:
break Loop
}
}
if idx != DelayedDNSResponseBufferSize {
t.Fatal("invalid delayedDNSResponse channel buffer size")
}
})
t.Run("tcpConnect has the expected buffer size", func(t *testing.T) {
ff := &testingx.FakeFiller{}
var idx int
Loop:
for {
ev := &model.ArchivalTCPConnectResult{}
ff.Fill(ev)
select {
case trace.tcpConnect <- ev:
idx++
default:
break Loop
}
}
if idx != TCPConnectBufferSize {
t.Fatal("invalid tcpConnect channel buffer size")
}
})
t.Run("tlsHandshake has the expected buffer size", func(t *testing.T) {
ff := &testingx.FakeFiller{}
var idx int
Loop:
for {
ev := &model.ArchivalTLSOrQUICHandshakeResult{}
ff.Fill(ev)
select {
case trace.tlsHandshake <- ev:
idx++
default:
break Loop
}
}
if idx != TLSHandshakeBufferSize {
t.Fatal("invalid tlsHandshake channel buffer size")
}
})
t.Run("quicHandshake has the expected buffer size", func(t *testing.T) {
ff := &testingx.FakeFiller{}
var idx int
Loop:
for {
ev := &model.ArchivalTLSOrQUICHandshakeResult{}
ff.Fill(ev)
select {
case trace.quicHandshake <- ev:
idx++
default:
break Loop
}
}
if idx != QUICHandshakeBufferSize {
t.Fatal("invalid quicHandshake channel buffer size")
}
})
t.Run("TimeNowFn is nil", func(t *testing.T) {
if trace.TimeNowFn != nil {
t.Fatal("expected nil TimeNowFn")
}
})
t.Run("ZeroTime", func(t *testing.T) {
if !trace.ZeroTime.Equal(zeroTime) {
t.Fatal("invalid zero time")
}
})
})
}
func TestTrace(t *testing.T) {
t.Run("NewStdlibResolverFn works as intended", func(t *testing.T) {
t.Run("when not nil", func(t *testing.T) {
mockedErr := errors.New("mocked")
tx := &Trace{
NewStdlibResolverFn: func(logger model.Logger) model.Resolver {
return &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return []string{}, mockedErr
},
}
},
}
resolver := tx.newStdlibResolver(model.DiscardLogger)
ctx := context.Background()
addrs, err := resolver.LookupHost(ctx, "example.com")
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if len(addrs) != 0 {
t.Fatal("expected array of size 0")
}
})
t.Run("when nil", func(t *testing.T) {
tx := &Trace{
NewParallelUDPResolverFn: nil,
}
resolver := tx.newStdlibResolver(model.DiscardLogger)
ctx, cancel := context.WithCancel(context.Background())
cancel()
addrs, err := resolver.LookupHost(ctx, "example.com")
if err == nil || err.Error() != netxlite.FailureInterrupted {
t.Fatal("unexpected err", err)
}
if len(addrs) != 0 {
t.Fatal("expected array of size 0")
}
})
})
t.Run("NewParallelUDPResolverFn works as intended", func(t *testing.T) {
t.Run("when not nil", func(t *testing.T) {
mockedErr := errors.New("mocked")
tx := &Trace{
NewParallelUDPResolverFn: func(logger model.Logger, dialer model.Dialer, address string) model.Resolver {
return &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return []string{}, mockedErr
},
}
},
}
dialer := &mocks.Dialer{}
resolver := tx.newParallelUDPResolver(model.DiscardLogger, dialer, "1.1.1.1:53")
ctx := context.Background()
addrs, err := resolver.LookupHost(ctx, "example.com")
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if len(addrs) != 0 {
t.Fatal("expected array of size 0")
}
})
t.Run("when nil", func(t *testing.T) {
tx := &Trace{
NewParallelUDPResolverFn: nil,
}
dialer := netxlite.NewDialerWithoutResolver(model.DiscardLogger)
resolver := tx.newParallelUDPResolver(model.DiscardLogger, dialer, "1.1.1.1:53")
ctx, cancel := context.WithCancel(context.Background())
cancel()
addrs, err := resolver.LookupHost(ctx, "example.com")
if err == nil || err.Error() != netxlite.FailureInterrupted {
t.Fatal("unexpected err", err)
}
if len(addrs) != 0 {
t.Fatal("expected array of size 0")
}
})
})
t.Run("NewParallelDNSOverHTTPSResolverFn works as intended", func(t *testing.T) {
t.Run("when not nil", func(t *testing.T) {
mockedErr := errors.New("mocked")
tx := &Trace{
NewParallelDNSOverHTTPSResolverFn: func(logger model.Logger, URL string) model.Resolver {
return &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return []string{}, mockedErr
},
}
},
}
resolver := tx.newParallelDNSOverHTTPSResolver(model.DiscardLogger, "https://dns.google.com")
ctx := context.Background()
addrs, err := resolver.LookupHost(ctx, "example.com")
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if len(addrs) != 0 {
t.Fatal("expected array of size 0")
}
})
t.Run("when nil", func(t *testing.T) {
tx := &Trace{
NewParallelDNSOverHTTPSResolverFn: nil,
}
resolver := tx.newParallelDNSOverHTTPSResolver(model.DiscardLogger, "https://dns.google.com")
ctx, cancel := context.WithCancel(context.Background())
cancel()
addrs, err := resolver.LookupHost(ctx, "example.com")
if err == nil || err.Error() != netxlite.FailureInterrupted {
t.Fatal("unexpected err", err)
}
if len(addrs) != 0 {
t.Fatal("expected array of size 0")
}
})
})
t.Run("NewDialerWithoutResolverFn works as intended", func(t *testing.T) {
t.Run("when not nil", func(t *testing.T) {
mockedErr := errors.New("mocked")
tx := &Trace{
NewDialerWithoutResolverFn: func(dl model.DebugLogger) model.Dialer {
return &mocks.Dialer{
MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, mockedErr
},
}
},
}
dialer := tx.NewDialerWithoutResolver(model.DiscardLogger)
ctx := context.Background()
conn, err := dialer.DialContext(ctx, "tcp", "1.1.1.1:443")
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if conn != nil {
t.Fatal("expected nil conn")
}
})
t.Run("when nil", func(t *testing.T) {
tx := &Trace{
NewDialerWithoutResolverFn: nil,
}
dialer := tx.NewDialerWithoutResolver(model.DiscardLogger)
ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately
conn, err := dialer.DialContext(ctx, "tcp", "1.1.1.1:443")
if err == nil || err.Error() != netxlite.FailureInterrupted {
t.Fatal("unexpected err", err)
}
if conn != nil {
t.Fatal("expected nil conn")
}
})
})
t.Run("NewTLSHandshakerStdlibFn works as intended", func(t *testing.T) {
t.Run("when not nil", func(t *testing.T) {
mockedErr := errors.New("mocked")
tx := &Trace{
NewTLSHandshakerStdlibFn: func(dl model.DebugLogger) model.TLSHandshaker {
return &mocks.TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return nil, tls.ConnectionState{}, mockedErr
},
}
},
}
thx := tx.NewTLSHandshakerStdlib(model.DiscardLogger)
ctx := context.Background()
conn, state, err := thx.Handshake(ctx, &mocks.Conn{}, &tls.Config{})
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if !reflect.ValueOf(state).IsZero() {
t.Fatal("state is not a zero value")
}
if conn != nil {
t.Fatal("expected nil conn")
}
})
t.Run("when nil", func(t *testing.T) {
mockedErr := errors.New("mocked")
tx := &Trace{
NewTLSHandshakerStdlibFn: nil,
}
thx := tx.NewTLSHandshakerStdlib(model.DiscardLogger)
tcpConn := &mocks.Conn{
MockSetDeadline: func(t time.Time) error {
return nil
},
MockRemoteAddr: func() net.Addr {
return &mocks.Addr{
MockNetwork: func() string {
return "tcp"
},
MockString: func() string {
return "1.1.1.1:443"
},
}
},
MockWrite: func(b []byte) (int, error) {
return 0, mockedErr
},
MockClose: func() error {
return nil
},
}
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}
ctx := context.Background()
conn, state, err := thx.Handshake(ctx, tcpConn, tlsConfig)
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if !reflect.ValueOf(state).IsZero() {
t.Fatal("state is not a zero value")
}
if conn != nil {
t.Fatal("expected nil conn")
}
})
})
t.Run("NewTLSHandshakerUTLSFn works as intended", func(t *testing.T) {
t.Run("when not nil", func(t *testing.T) {
mockedErr := errors.New("mocked")
tx := &Trace{
NewTLSHandshakerUTLSFn: func(dl model.DebugLogger, id *utls.ClientHelloID) model.TLSHandshaker {
return &mocks.TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return nil, tls.ConnectionState{}, mockedErr
},
}
},
}
thx := tx.NewTLSHandshakerUTLS(model.DiscardLogger, &utls.HelloGolang)
ctx := context.Background()
conn, state, err := thx.Handshake(ctx, &mocks.Conn{}, &tls.Config{})
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if !reflect.ValueOf(state).IsZero() {
t.Fatal("state is not a zero value")
}
if conn != nil {
t.Fatal("expected nil conn")
}
})
t.Run("when nil", func(t *testing.T) {
mockedErr := errors.New("mocked")
tx := &Trace{
NewTLSHandshakerStdlibFn: nil,
}
thx := tx.newTLSHandshakerUTLS(model.DiscardLogger, &utls.HelloGolang)
tcpConn := &mocks.Conn{
MockSetDeadline: func(t time.Time) error {
return nil
},
MockRemoteAddr: func() net.Addr {
return &mocks.Addr{
MockNetwork: func() string {
return "tcp"
},
MockString: func() string {
return "1.1.1.1:443"
},
}
},
MockWrite: func(b []byte) (int, error) {
return 0, mockedErr
},
MockClose: func() error {
return nil
},
}
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}
ctx := context.Background()
conn, state, err := thx.Handshake(ctx, tcpConn, tlsConfig)
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if !reflect.ValueOf(state).IsZero() {
t.Fatal("state is not a zero value")
}
if conn != nil {
t.Fatal("expected nil conn")
}
})
})
t.Run("NewQUICDialerWithoutResolverFn works as intended", func(t *testing.T) {
t.Run("when not nil", func(t *testing.T) {
mockedErr := errors.New("mocked")
tx := &Trace{
NewQUICDialerWithoutResolverFn: func(listener model.QUICListener, dl model.DebugLogger) model.QUICDialer {
return &mocks.QUICDialer{
MockDialContext: func(ctx context.Context, address string,
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
return nil, mockedErr
},
}
},
}
qdx := tx.newQUICDialerWithoutResolver(&mocks.QUICListener{}, model.DiscardLogger)
ctx := context.Background()
qconn, err := qdx.DialContext(ctx, "1.1.1.1:443", &tls.Config{}, &quic.Config{})
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if qconn != nil {
t.Fatal("expected nil conn")
}
})
t.Run("when nil", func(t *testing.T) {
mockedErr := errors.New("mocked")
tx := &Trace{
NewQUICDialerWithoutResolverFn: nil, // explicit
}
pconn := &mocks.UDPLikeConn{
MockLocalAddr: func() net.Addr {
return &net.UDPAddr{
Port: 0,
}
},
MockRemoteAddr: func() net.Addr {
return &net.UDPAddr{
Port: 0,
}
},
MockSyscallConn: func() (syscall.RawConn, error) {
return nil, mockedErr
},
MockClose: func() error {
return nil
},
}
listener := &mocks.QUICListener{
MockListen: func(addr *net.UDPAddr) (model.UDPLikeConn, error) {
return pconn, nil
},
}
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}
dialer := tx.newQUICDialerWithoutResolver(listener, model.DiscardLogger)
ctx := context.Background()
qconn, err := dialer.DialContext(ctx, "1.1.1.1:443", tlsConfig, &quic.Config{})
if !errors.Is(err, mockedErr) {
t.Fatal("unexpected err", err)
}
if qconn != nil {
t.Fatal("expected nil conn")
}
})
})
t.Run("TimeNowFn works as intended", func(t *testing.T) {
fixedTime := time.Date(2022, 01, 01, 00, 00, 00, 00, time.UTC)
tx := &Trace{
TimeNowFn: func() time.Time {
return fixedTime
},
}
if !tx.TimeNow().Equal(fixedTime) {
t.Fatal("we cannot override time.Now calls")
}
})
t.Run("TimeSince works as intended", func(t *testing.T) {
t0 := time.Date(2022, 01, 01, 00, 00, 00, 00, time.UTC)
t1 := t0.Add(10 * time.Second)
tx := &Trace{
TimeNowFn: func() time.Time {
return t1
},
}
if tx.TimeSince(t0) != 10*time.Second {
t.Fatal("apparently Trace.Since is broken")
}
})
}