package connect import ( "context" "crypto/tls" "log" "net" "net/http" "os" "time" "github.com/hashicorp/consul/api" ) // Service represents a Consul service that accepts and/or connects via Connect. // This can represent a service that only is a server, only is a client, or // both. // // TODO(banks): API for monitoring status of certs from app // // TODO(banks): Agent implicit health checks based on knowing which certs are // available should prevent clients being routed until the agent knows the // service has been delivered valid certificates. Once built, document that here // too. type Service struct { // serviceID is the unique ID for this service in the agent-local catalog. // This is often but not always the service name. This is used to request // Connect metadata. If the service with this ID doesn't exist on the local // agent no error will be returned and the Service will retry periodically. // This allows service startup and registration to happen in either order // without coordination since they might be performed by separate processes. serviceID string // client is the Consul API client. It must be configured with an appropriate // Token that has `service:write` policy on the provided ServiceID. If an // insufficient token is provided, the Service will abort further attempts to // fetch certificates and print a loud error message. It will not Close() or // kill the process since that could lead to a crash loop in every service if // ACL token was revoked. All attempts to dial will error and any incoming // connections will fail to verify. client *api.Client // serverTLSCfg is the (reloadable) TLS config we use for serving. serverTLSCfg *ReloadableTLSConfig // clientTLSCfg is the (reloadable) TLS config we use for dialling. clientTLSCfg *ReloadableTLSConfig logger *log.Logger } // NewService creates and starts a Service. The caller must close the returned // service to free resources and allow the program to exit normally. This is // typically called in a signal handler. func NewService(serviceID string, client *api.Client) (*Service, error) { return NewServiceWithLogger(serviceID, client, log.New(os.Stderr, "", log.LstdFlags)) } // NewServiceWithLogger starts the service with a specified log.Logger. func NewServiceWithLogger(serviceID string, client *api.Client, logger *log.Logger) (*Service, error) { s := &Service{ serviceID: serviceID, client: client, logger: logger, } s.serverTLSCfg = NewReloadableTLSConfig(defaultTLSConfig(serverVerifyCerts)) s.clientTLSCfg = NewReloadableTLSConfig(defaultTLSConfig(clientVerifyCerts)) // TODO(banks) run the background certificate sync return s, nil } // NewDevServiceFromCertFiles creates a Service using certificate and key files // passed instead of fetching them from the client. func NewDevServiceFromCertFiles(serviceID string, client *api.Client, logger *log.Logger, caFile, certFile, keyFile string) (*Service, error) { s := &Service{ serviceID: serviceID, client: client, logger: logger, } tlsCfg, err := devTLSConfigFromFiles(caFile, certFile, keyFile) if err != nil { return nil, err } // Note that NewReloadableTLSConfig makes a copy so we can re-use the same // base for both client and server with swapped verifiers. tlsCfg.VerifyPeerCertificate = serverVerifyCerts s.serverTLSCfg = NewReloadableTLSConfig(tlsCfg) tlsCfg.VerifyPeerCertificate = clientVerifyCerts s.clientTLSCfg = NewReloadableTLSConfig(tlsCfg) return s, nil } // ServerTLSConfig returns a *tls.Config that allows any TCP listener to accept // and authorize incoming Connect clients. It will return a single static config // with hooks to dynamically load certificates, and perform Connect // authorization during verification. Service implementations do not need to // reload this to get new certificates. // // At any time it may be possible that the Service instance does not have access // to usable certificates due to not being initially setup yet or a prolonged // error during renewal. The listener will be able to accept connections again // once connectivity is restored provided the client's Token is valid. func (s *Service) ServerTLSConfig() *tls.Config { return s.serverTLSCfg.TLSConfig() } // Dial connects to a remote Connect-enabled server. The passed Resolver is used // to discover a single candidate instance which will be dialled and have it's // TLS certificate verified against the expected identity. Failures are returned // directly with no retries. Repeated dials may use different instances // depending on the Resolver implementation. // // Timeout can be managed via the Context. func (s *Service) Dial(ctx context.Context, resolver Resolver) (net.Conn, error) { addr, certURI, err := resolver.Resolve(ctx) if err != nil { return nil, err } var dialer net.Dialer tcpConn, err := dialer.DialContext(ctx, "tcp", addr) if err != nil { return nil, err } tlsConn := tls.Client(tcpConn, s.clientTLSCfg.TLSConfig()) // Set deadline for Handshake to complete. deadline, ok := ctx.Deadline() if ok { tlsConn.SetDeadline(deadline) } err = tlsConn.Handshake() if err != nil { tlsConn.Close() return nil, err } // Clear deadline since that was only for connection. Caller can set their own // deadline later as necessary. tlsConn.SetDeadline(time.Time{}) // Verify that the connect server's URI matches certURI err = verifyServerCertMatchesURI(tlsConn.ConnectionState().PeerCertificates, certURI) if err != nil { tlsConn.Close() return nil, err } return tlsConn, nil } // HTTPDialContext is compatible with http.Transport.DialContext. It expects the // addr hostname to be specified using Consul DNS query syntax, e.g. // "web.service.consul". It converts that into the equivalent ConsulResolver and // then call s.Dial with the resolver. This is low level, clients should // typically use HTTPClient directly. func (s *Service) HTTPDialContext(ctx context.Context, network, addr string) (net.Conn, error) { var r ConsulResolver // TODO(banks): parse addr into ConsulResolver return s.Dial(ctx, &r) } // HTTPClient returns an *http.Client configured to dial remote Consul Connect // HTTP services. The client will return an error if attempting to make requests // to a non HTTPS hostname. It resolves the domain of the request with the same // syntax as Consul DNS queries although it performs discovery directly via the // API rather than just relying on Consul DNS. Hostnames that are not valid // Consul DNS queries will fail. func (s *Service) HTTPClient() *http.Client { return &http.Client{ Transport: &http.Transport{ DialContext: s.HTTPDialContext, }, } } // Close stops the service and frees resources. func (s *Service) Close() { // TODO(banks): stop background activity if started }