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 }