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") } }) }