clientv3: Set authority used in cert checks to host of endpoint

This commit is contained in:
Joe Betz 2019-09-24 23:55:25 -07:00
parent 129138760c
commit 97388ce454
No known key found for this signature in database
GPG Key ID: 4930C680B6E0DDB8
4 changed files with 72 additions and 36 deletions

1
.words
View File

@ -95,6 +95,7 @@ jitter
WithBackoff WithBackoff
BackoffLinearWithJitter BackoffLinearWithJitter
jitter jitter
WithDialer
WithMax WithMax
ServerStreams ServerStreams
BidiStreams BidiStreams

View File

@ -16,7 +16,9 @@
package endpoint package endpoint
import ( import (
"context"
"fmt" "fmt"
"net"
"net/url" "net/url"
"strings" "strings"
"sync" "sync"
@ -238,3 +240,19 @@ func ParseHostPort(hostPort string) (host string, port string) {
} }
return host, port return host, port
} }
// Dialer dials a endpoint using net.Dialer.
// Context cancelation and timeout are supported.
func Dialer(ctx context.Context, dialEp string) (net.Conn, error) {
proto, host, _ := ParseEndpoint(dialEp)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
dialer := &net.Dialer{}
if deadline, ok := ctx.Deadline(); ok {
dialer.Deadline = deadline
}
return dialer.DialContext(ctx, proto, host)
}

View File

@ -230,24 +230,17 @@ func (c *Client) dialSetupOpts(creds grpccredentials.TransportCredentials, dopts
} }
opts = append(opts, dopts...) opts = append(opts, dopts...)
// Provide a net dialer that supports cancelation and timeout. dialer := endpoint.Dialer
f := func(dialEp string, t time.Duration) (net.Conn, error) {
proto, host, _ := endpoint.ParseEndpoint(dialEp)
select {
case <-c.ctx.Done():
return nil, c.ctx.Err()
default:
}
dialer := &net.Dialer{Timeout: t}
return dialer.DialContext(c.ctx, proto, host)
}
opts = append(opts, grpc.WithDialer(f))
if creds != nil { if creds != nil {
opts = append(opts, grpc.WithTransportCredentials(creds)) opts = append(opts, grpc.WithTransportCredentials(creds))
// gRPC load balancer workaround. See credentials.transportCredential for details.
if credsDialer, ok := creds.(TransportCredentialsWithDialer); ok {
dialer = credsDialer.Dialer
}
} else { } else {
opts = append(opts, grpc.WithInsecure()) opts = append(opts, grpc.WithInsecure())
} }
opts = append(opts, grpc.WithContextDialer(dialer))
// Interceptor retry and backoff. // Interceptor retry and backoff.
// TODO: Replace all of clientv3/retry.go with interceptor based retry, or with // TODO: Replace all of clientv3/retry.go with interceptor based retry, or with
@ -663,3 +656,9 @@ func IsConnCanceled(err error) bool {
// <= gRPC v1.7.x returns 'errors.New("grpc: the client connection is closing")' // <= gRPC v1.7.x returns 'errors.New("grpc: the client connection is closing")'
return strings.Contains(err.Error(), "grpc: the client connection is closing") return strings.Contains(err.Error(), "grpc: the client connection is closing")
} }
// TransportCredentialsWithDialer is for a gRPC load balancer workaround. See credentials.transportCredential for details.
type TransportCredentialsWithDialer interface {
grpccredentials.TransportCredentials
Dialer(ctx context.Context, dialEp string) (net.Conn, error)
}

View File

@ -22,6 +22,7 @@ import (
"net" "net"
"sync" "sync"
"go.etcd.io/etcd/clientv3/balancer/resolver/endpoint"
"go.etcd.io/etcd/etcdserver/api/v3rpc/rpctypes" "go.etcd.io/etcd/etcdserver/api/v3rpc/rpctypes"
grpccredentials "google.golang.org/grpc/credentials" grpccredentials "google.golang.org/grpc/credentials"
) )
@ -65,38 +66,37 @@ func (b *bundle) NewWithMode(mode string) (grpccredentials.Bundle, error) {
} }
// transportCredential implements "grpccredentials.TransportCredentials" interface. // transportCredential implements "grpccredentials.TransportCredentials" interface.
// transportCredential wraps TransportCredentials to track which
// addresses are dialed for which endpoints, and then sets the authority when checking the endpoint's cert to the
// hostname or IP of the dialed endpoint.
// This is a workaround of a gRPC load balancer issue. gRPC uses the dialed target's service name as the authority when
// checking all endpoint certs, which does not work for etcd servers using their hostname or IP as the Subject Alternative Name
// in their TLS certs.
// To enable, include both WithTransportCredentials(creds) and WithContextDialer(creds.Dialer)
// when dialing.
type transportCredential struct { type transportCredential struct {
gtc grpccredentials.TransportCredentials gtc grpccredentials.TransportCredentials
mu sync.Mutex
// addrToEndpoint maps from the connection addresses that are dialed to the hostname or IP of the
// endpoint provided to the dialer when dialing
addrToEndpoint map[string]string
} }
func newTransportCredential(cfg *tls.Config) *transportCredential { func newTransportCredential(cfg *tls.Config) *transportCredential {
return &transportCredential{ return &transportCredential{
gtc: grpccredentials.NewTLS(cfg), gtc: grpccredentials.NewTLS(cfg),
addrToEndpoint: map[string]string{},
} }
} }
func (tc *transportCredential) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, grpccredentials.AuthInfo, error) { func (tc *transportCredential) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, grpccredentials.AuthInfo, error) {
// Only overwrite when authority is an IP address! // Set the authority when checking the endpoint's cert to the hostname or IP of the dialed endpoint
// Let's say, a server runs SRV records on "etcd.local" that resolves tc.mu.Lock()
// to "m1.etcd.local", and its SAN field also includes "m1.etcd.local". dialEp, ok := tc.addrToEndpoint[rawConn.RemoteAddr().String()]
// But what if SAN does not include its resolved IP address (e.g. 127.0.0.1)? tc.mu.Unlock()
// Then, the server should only authenticate using its DNS hostname "m1.etcd.local", if ok {
// instead of overwriting it with its IP address. _, host, _ := endpoint.ParseEndpoint(dialEp)
// And we do not overwrite "localhost" either. Only overwrite IP addresses! authority = host
if isIP(authority) {
target := rawConn.RemoteAddr().String()
if authority != target {
// When user dials with "grpc.WithDialer", "grpc.DialContext" "cc.parsedTarget"
// update only happens once. This is problematic, because when TLS is enabled,
// retries happen through "grpc.WithDialer" with static "cc.parsedTarget" from
// the initial dial call.
// If the server authenticates by IP addresses, we want to set a new endpoint as
// a new authority. Otherwise
// "transport: authentication handshake failed: x509: certificate is valid for 127.0.0.1, 192.168.121.180, not 192.168.223.156"
// when the new dial target is "192.168.121.180" whose certificate host name is also "192.168.121.180"
// but client tries to authenticate with previously set "cc.parsedTarget" field "192.168.223.156"
authority = target
}
} }
return tc.gtc.ClientHandshake(ctx, authority, rawConn) return tc.gtc.ClientHandshake(ctx, authority, rawConn)
} }
@ -115,8 +115,15 @@ func (tc *transportCredential) Info() grpccredentials.ProtocolInfo {
} }
func (tc *transportCredential) Clone() grpccredentials.TransportCredentials { func (tc *transportCredential) Clone() grpccredentials.TransportCredentials {
copy := map[string]string{}
tc.mu.Lock()
for k, v := range tc.addrToEndpoint {
copy[k] = v
}
tc.mu.Unlock()
return &transportCredential{ return &transportCredential{
gtc: tc.gtc.Clone(), gtc: tc.gtc.Clone(),
addrToEndpoint: copy,
} }
} }
@ -124,6 +131,17 @@ func (tc *transportCredential) OverrideServerName(serverNameOverride string) err
return tc.gtc.OverrideServerName(serverNameOverride) return tc.gtc.OverrideServerName(serverNameOverride)
} }
func (tc *transportCredential) Dialer(ctx context.Context, dialEp string) (net.Conn, error) {
// Keep track of which addresses are dialed for which endpoints
conn, err := endpoint.Dialer(ctx, dialEp)
if conn != nil {
tc.mu.Lock()
tc.addrToEndpoint[conn.RemoteAddr().String()] = dialEp
tc.mu.Unlock()
}
return conn, err
}
// perRPCCredential implements "grpccredentials.PerRPCCredentials" interface. // perRPCCredential implements "grpccredentials.PerRPCCredentials" interface.
type perRPCCredential struct { type perRPCCredential struct {
authToken string authToken string