github.com/outbrain/consul@v1.4.5/connect/service.go (about)

     1  package connect
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	"errors"
     8  	"log"
     9  	"net"
    10  	"net/http"
    11  	"os"
    12  	"time"
    13  
    14  	"github.com/hashicorp/consul/api"
    15  	"github.com/hashicorp/consul/watch"
    16  	"golang.org/x/net/http2"
    17  )
    18  
    19  // Service represents a Consul service that accepts and/or connects via Connect.
    20  // This can represent a service that only is a server, only is a client, or
    21  // both.
    22  //
    23  // TODO(banks): Agent implicit health checks based on knowing which certs are
    24  // available should prevent clients being routed until the agent knows the
    25  // service has been delivered valid certificates. Once built, document that here
    26  // too.
    27  type Service struct {
    28  	// service is the name (not ID) for the Consul service. This is used to request
    29  	// Connect metadata.
    30  	service string
    31  
    32  	// client is the Consul API client. It must be configured with an appropriate
    33  	// Token that has `service:write` policy on the provided service. If an
    34  	// insufficient token is provided, the Service will abort further attempts to
    35  	// fetch certificates and print a loud error message. It will not Close() or
    36  	// kill the process since that could lead to a crash loop in every service if
    37  	// ACL token was revoked. All attempts to dial will error and any incoming
    38  	// connections will fail to verify. It may be nil if the Service is being
    39  	// configured from local files for development or testing.
    40  	client *api.Client
    41  
    42  	// tlsCfg is the dynamic TLS config
    43  	tlsCfg *dynamicTLSConfig
    44  
    45  	// httpResolverFromAddr is a function that returns a Resolver from a string
    46  	// address for HTTP clients. It's privately pluggable to make testing easier
    47  	// but will default to a simple method to parse the host as a Consul DNS host.
    48  	httpResolverFromAddr func(addr string) (Resolver, error)
    49  
    50  	rootsWatch *watch.Plan
    51  	leafWatch  *watch.Plan
    52  
    53  	logger *log.Logger
    54  }
    55  
    56  // NewService creates and starts a Service. The caller must close the returned
    57  // service to free resources and allow the program to exit normally. This is
    58  // typically called in a signal handler.
    59  //
    60  // Caller must provide client which is already configured to speak to the local
    61  // Consul agent, and with an ACL token that has `service:write` privileges for
    62  // the service specified.
    63  func NewService(serviceName string, client *api.Client) (*Service, error) {
    64  	return NewServiceWithLogger(serviceName, client,
    65  		log.New(os.Stderr, "", log.LstdFlags))
    66  }
    67  
    68  // NewServiceWithLogger starts the service with a specified log.Logger.
    69  func NewServiceWithLogger(serviceName string, client *api.Client,
    70  	logger *log.Logger) (*Service, error) {
    71  	s := &Service{
    72  		service:              serviceName,
    73  		client:               client,
    74  		logger:               logger,
    75  		tlsCfg:               newDynamicTLSConfig(defaultTLSConfig(), logger),
    76  		httpResolverFromAddr: ConsulResolverFromAddrFunc(client),
    77  	}
    78  
    79  	// Set up root and leaf watches
    80  	p, err := watch.Parse(map[string]interface{}{
    81  		"type": "connect_roots",
    82  	})
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	s.rootsWatch = p
    87  	s.rootsWatch.HybridHandler = s.rootsWatchHandler
    88  
    89  	p, err = watch.Parse(map[string]interface{}{
    90  		"type":    "connect_leaf",
    91  		"service": s.service,
    92  	})
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	s.leafWatch = p
    97  	s.leafWatch.HybridHandler = s.leafWatchHandler
    98  
    99  	go s.rootsWatch.RunWithClientAndLogger(client, s.logger)
   100  	go s.leafWatch.RunWithClientAndLogger(client, s.logger)
   101  
   102  	return s, nil
   103  }
   104  
   105  // NewDevServiceFromCertFiles creates a Service using certificate and key files
   106  // passed instead of fetching them from the client.
   107  func NewDevServiceFromCertFiles(serviceID string, logger *log.Logger,
   108  	caFile, certFile, keyFile string) (*Service, error) {
   109  
   110  	tlsCfg, err := devTLSConfigFromFiles(caFile, certFile, keyFile)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	return NewDevServiceWithTLSConfig(serviceID, logger, tlsCfg)
   115  }
   116  
   117  // NewDevServiceWithTLSConfig creates a Service using static TLS config passed.
   118  // It's mostly useful for testing.
   119  func NewDevServiceWithTLSConfig(serviceName string, logger *log.Logger,
   120  	tlsCfg *tls.Config) (*Service, error) {
   121  	s := &Service{
   122  		service: serviceName,
   123  		logger:  logger,
   124  		tlsCfg:  newDynamicTLSConfig(tlsCfg, logger),
   125  	}
   126  	return s, nil
   127  }
   128  
   129  // Name returns the name of the service this object represents. Note it is the
   130  // service _name_ as used during discovery, not the ID used to uniquely identify
   131  // an instance of the service with an agent.
   132  func (s *Service) Name() string {
   133  	return s.service
   134  }
   135  
   136  // ServerTLSConfig returns a *tls.Config that allows any TCP listener to accept
   137  // and authorize incoming Connect clients. It will return a single static config
   138  // with hooks to dynamically load certificates, and perform Connect
   139  // authorization during verification. Service implementations do not need to
   140  // reload this to get new certificates.
   141  //
   142  // At any time it may be possible that the Service instance does not have access
   143  // to usable certificates due to not being initially setup yet or a prolonged
   144  // error during renewal. The listener will be able to accept connections again
   145  // once connectivity is restored provided the client's Token is valid.
   146  //
   147  // To prevent routing traffic to the app instance while it's certificates are
   148  // invalid or not populated yet you may use Ready in a health check endpoint
   149  // and/or ReadyWait during startup before starting the TLS listener. The latter
   150  // only prevents connections during initial bootstrap (including permission
   151  // issues where certs can never be issued due to bad credentials) but won't
   152  // handle the case that certificates expire and an error prevents timely
   153  // renewal.
   154  func (s *Service) ServerTLSConfig() *tls.Config {
   155  	return s.tlsCfg.Get(newServerSideVerifier(s.client, s.service))
   156  }
   157  
   158  // Dial connects to a remote Connect-enabled server. The passed Resolver is used
   159  // to discover a single candidate instance which will be dialed and have it's
   160  // TLS certificate verified against the expected identity. Failures are returned
   161  // directly with no retries. Repeated dials may use different instances
   162  // depending on the Resolver implementation.
   163  //
   164  // Timeout can be managed via the Context.
   165  //
   166  // Calls to Dial made before the Service has loaded certificates from the agent
   167  // will fail. You can prevent this by using Ready or ReadyWait in app during
   168  // startup.
   169  func (s *Service) Dial(ctx context.Context, resolver Resolver) (net.Conn, error) {
   170  	addr, certURI, err := resolver.Resolve(ctx)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  	s.logger.Printf("[DEBUG] resolved service instance: %s (%s)", addr,
   175  		certURI.URI())
   176  	var dialer net.Dialer
   177  	tcpConn, err := dialer.DialContext(ctx, "tcp", addr)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	tlsConn := tls.Client(tcpConn, s.tlsCfg.Get(clientSideVerifier))
   183  	// Set deadline for Handshake to complete.
   184  	deadline, ok := ctx.Deadline()
   185  	if ok {
   186  		tlsConn.SetDeadline(deadline)
   187  	}
   188  	// Perform handshake
   189  	if err = tlsConn.Handshake(); err != nil {
   190  		tlsConn.Close()
   191  		return nil, err
   192  	}
   193  	// Clear deadline since that was only for connection. Caller can set their own
   194  	// deadline later as necessary.
   195  	tlsConn.SetDeadline(time.Time{})
   196  
   197  	// Verify that the connect server's URI matches certURI
   198  	err = verifyServerCertMatchesURI(tlsConn.ConnectionState().PeerCertificates,
   199  		certURI)
   200  	if err != nil {
   201  		tlsConn.Close()
   202  		return nil, err
   203  	}
   204  	s.logger.Printf("[DEBUG] successfully connected to %s (%s)", addr,
   205  		certURI.URI())
   206  	return tlsConn, nil
   207  }
   208  
   209  // HTTPDialTLS is compatible with http.Transport.DialTLS. It expects the addr
   210  // hostname to be specified using Consul DNS query syntax, e.g.
   211  // "web.service.consul". It converts that into the equivalent ConsulResolver and
   212  // then call s.Dial with the resolver. This is low level, clients should
   213  // typically use HTTPClient directly.
   214  func (s *Service) HTTPDialTLS(network,
   215  	addr string) (net.Conn, error) {
   216  	if s.httpResolverFromAddr == nil {
   217  		return nil, errors.New("no http resolver configured")
   218  	}
   219  	r, err := s.httpResolverFromAddr(addr)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  	// TODO(banks): figure out how to do timeouts better.
   224  	return s.Dial(context.Background(), r)
   225  }
   226  
   227  // HTTPClient returns an *http.Client configured to dial remote Consul Connect
   228  // HTTP services. The client will return an error if attempting to make requests
   229  // to a non HTTPS hostname. It resolves the domain of the request with the same
   230  // syntax as Consul DNS queries although it performs discovery directly via the
   231  // API rather than just relying on Consul DNS. Hostnames that are not valid
   232  // Consul DNS queries will fail.
   233  func (s *Service) HTTPClient() *http.Client {
   234  	t := &http.Transport{
   235  		// Sadly we can't use DialContext hook since that is expected to return a
   236  		// plain TCP connection and http.Client tries to start a TLS handshake over
   237  		// it. We need to control the handshake to be able to do our validation.
   238  		// So we have to use the older DialTLS which means no context/timeout
   239  		// support.
   240  		//
   241  		// TODO(banks): figure out how users can configure a timeout when using
   242  		// this and/or compatibility with http.Request.WithContext.
   243  		DialTLS: s.HTTPDialTLS,
   244  	}
   245  	// Need to manually re-enable http2 support since we set custom DialTLS.
   246  	// See https://golang.org/src/net/http/transport.go?s=8692:9036#L228
   247  	http2.ConfigureTransport(t)
   248  	return &http.Client{
   249  		Transport: t,
   250  	}
   251  }
   252  
   253  // Close stops the service and frees resources.
   254  func (s *Service) Close() error {
   255  	if s.rootsWatch != nil {
   256  		s.rootsWatch.Stop()
   257  	}
   258  	if s.leafWatch != nil {
   259  		s.leafWatch.Stop()
   260  	}
   261  	return nil
   262  }
   263  
   264  func (s *Service) rootsWatchHandler(blockParam watch.BlockingParamVal, raw interface{}) {
   265  	if raw == nil {
   266  		return
   267  	}
   268  	v, ok := raw.(*api.CARootList)
   269  	if !ok || v == nil {
   270  		s.logger.Println("[ERR] got invalid response from root watch")
   271  		return
   272  	}
   273  
   274  	// Got new root certificates, update the tls.Configs.
   275  	roots := x509.NewCertPool()
   276  	for _, root := range v.Roots {
   277  		roots.AppendCertsFromPEM([]byte(root.RootCertPEM))
   278  	}
   279  
   280  	s.tlsCfg.SetRoots(roots)
   281  }
   282  
   283  func (s *Service) leafWatchHandler(blockParam watch.BlockingParamVal, raw interface{}) {
   284  	if raw == nil {
   285  		return // ignore
   286  	}
   287  	v, ok := raw.(*api.LeafCert)
   288  	if !ok || v == nil {
   289  		s.logger.Println("[ERR] got invalid response from root watch")
   290  		return
   291  	}
   292  
   293  	// Got new leaf, update the tls.Configs
   294  	cert, err := tls.X509KeyPair([]byte(v.CertPEM), []byte(v.PrivateKeyPEM))
   295  	if err != nil {
   296  		s.logger.Printf("[ERR] failed to parse new leaf cert: %s", err)
   297  		return
   298  	}
   299  
   300  	s.tlsCfg.SetLeaf(&cert)
   301  }
   302  
   303  // Ready returns whether or not both roots and a leaf certificate are
   304  // configured. If both are non-nil, they are assumed to be valid and usable.
   305  func (s *Service) Ready() bool {
   306  	return s.tlsCfg.Ready()
   307  }
   308  
   309  // ReadyWait returns a chan that is closed when the the Service becomes ready
   310  // for use for the first time. Note that if the Service is ready when it is
   311  // called it returns a nil chan. Ready means that it has root and leaf
   312  // certificates configured which we assume are valid. The service may
   313  // subsequently stop being "ready" if it's certificates expire or are revoked
   314  // and an error prevents new ones being loaded but this method will not stop
   315  // returning a nil chan in that case. It is only useful for initial startup. For
   316  // ongoing health Ready() should be used.
   317  func (s *Service) ReadyWait() <-chan struct{} {
   318  	return s.tlsCfg.ReadyWait()
   319  }