github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/client/proxy/client.go (about)

     1  // Copyright 2023 Gravitational, Inc
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package proxy
    16  
    17  import (
    18  	"context"
    19  	"crypto/tls"
    20  	"encoding/asn1"
    21  	"net"
    22  	"slices"
    23  	"sync/atomic"
    24  	"time"
    25  
    26  	"github.com/gravitational/trace"
    27  	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    28  	"golang.org/x/crypto/ssh"
    29  	"golang.org/x/crypto/ssh/agent"
    30  	"google.golang.org/grpc"
    31  	"google.golang.org/grpc/credentials"
    32  	"google.golang.org/grpc/credentials/insecure"
    33  
    34  	"github.com/gravitational/teleport/api/breaker"
    35  	"github.com/gravitational/teleport/api/client"
    36  	authpb "github.com/gravitational/teleport/api/client/proto"
    37  	"github.com/gravitational/teleport/api/client/proxy/transport/transportv1"
    38  	"github.com/gravitational/teleport/api/defaults"
    39  	transportv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/transport/v1"
    40  	"github.com/gravitational/teleport/api/metadata"
    41  	"github.com/gravitational/teleport/api/utils/grpc/interceptors"
    42  )
    43  
    44  // ClientConfig contains configuration needed for a Client
    45  // to be able to connect to the cluster.
    46  type ClientConfig struct {
    47  	// ProxyAddress is the address of the Proxy server.
    48  	ProxyAddress string
    49  	// TLSRoutingEnabled indicates if the cluster is using TLS Routing.
    50  	TLSRoutingEnabled bool
    51  	// TLSConfigFunc produces the [tls.Config] required for mTLS connections to a specific cluster.
    52  	TLSConfigFunc func(cluster string) (*tls.Config, error)
    53  	// UnaryInterceptors are optional [grpc.UnaryClientInterceptor] to apply
    54  	// to the gRPC client.
    55  	UnaryInterceptors []grpc.UnaryClientInterceptor
    56  	// StreamInterceptors are optional [grpc.StreamClientInterceptor] to apply
    57  	// to the gRPC client.
    58  	StreamInterceptors []grpc.StreamClientInterceptor
    59  	// SSHConfig is the [ssh.ClientConfig] used to connect to the Proxy SSH server.
    60  	SSHConfig *ssh.ClientConfig
    61  	// DialTimeout defines how long to attempt dialing before timing out.
    62  	DialTimeout time.Duration
    63  	// DialOpts define options for dialing the client connection.
    64  	DialOpts []grpc.DialOption
    65  	// ALPNConnUpgradeRequired indicates that ALPN connection upgrades are
    66  	// required for making TLS routing requests.
    67  	ALPNConnUpgradeRequired bool
    68  	// InsecureSkipVerify is an option to skip HTTPS cert check
    69  	InsecureSkipVerify bool
    70  	// ViaJumpHost indicates if the connection to the cluster is direct
    71  	// or via another cluster.
    72  	ViaJumpHost bool
    73  	// PROXYHeaderGetter is used if present to get signed PROXY headers to propagate client's IP.
    74  	// Used by proxy's web server to make calls on behalf of connected clients.
    75  	PROXYHeaderGetter client.PROXYHeaderGetter
    76  
    77  	// The below items are intended to be used by tests to connect without mTLS.
    78  	// The gRPC transport credentials to use when establishing the connection to proxy.
    79  	creds func(cluster string) (credentials.TransportCredentials, error)
    80  	// The client credentials to use when establishing the connection to auth.
    81  	clientCreds func(cluster string) (client.Credentials, error)
    82  }
    83  
    84  // CheckAndSetDefaults ensures required options are present and
    85  // sets the default value of any that are omitted.
    86  func (c *ClientConfig) CheckAndSetDefaults() error {
    87  	if c.ProxyAddress == "" {
    88  		return trace.BadParameter("missing required parameter ProxyAddress")
    89  	}
    90  	if c.SSHConfig == nil {
    91  		return trace.BadParameter("missing required parameter SSHConfig")
    92  	}
    93  	if c.DialTimeout <= 0 {
    94  		c.DialTimeout = defaults.DefaultIOTimeout
    95  	}
    96  	if c.TLSConfigFunc != nil {
    97  		c.clientCreds = func(cluster string) (client.Credentials, error) {
    98  			cfg, err := c.TLSConfigFunc(cluster)
    99  			if err != nil {
   100  				return nil, trace.Wrap(err)
   101  			}
   102  
   103  			return client.LoadTLS(cfg), nil
   104  		}
   105  		c.creds = func(cluster string) (credentials.TransportCredentials, error) {
   106  			tlsCfg, err := c.TLSConfigFunc(cluster)
   107  			if err != nil {
   108  				return nil, trace.Wrap(err)
   109  			}
   110  			if !slices.Contains(tlsCfg.NextProtos, protocolProxySSHGRPC) {
   111  				tlsCfg.NextProtos = append(tlsCfg.NextProtos, protocolProxySSHGRPC)
   112  			}
   113  
   114  			// This logic still appears to be necessary to force client to always send
   115  			// a certificate regardless of the server setting. Otherwise the client may pick
   116  			// not to send the client certificate by looking at certificate request.
   117  			if len(tlsCfg.Certificates) > 0 {
   118  				cert := tlsCfg.Certificates[0]
   119  				tlsCfg.Certificates = nil
   120  				tlsCfg.GetClientCertificate = func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
   121  					return &cert, nil
   122  				}
   123  			}
   124  
   125  			return credentials.NewTLS(tlsCfg), nil
   126  		}
   127  	} else {
   128  		c.clientCreds = func(cluster string) (client.Credentials, error) {
   129  			return insecureCredentials{}, nil
   130  		}
   131  		c.creds = func(cluster string) (credentials.TransportCredentials, error) {
   132  			return insecure.NewCredentials(), nil
   133  		}
   134  	}
   135  
   136  	return nil
   137  }
   138  
   139  // insecureCredentials implements [client.Credentials] and is used by tests
   140  // to connect to the Auth server without mTLS.
   141  type insecureCredentials struct{}
   142  
   143  func (mc insecureCredentials) TLSConfig() (*tls.Config, error) {
   144  	return nil, nil
   145  }
   146  
   147  func (mc insecureCredentials) SSHClientConfig() (*ssh.ClientConfig, error) {
   148  	return nil, trace.NotImplemented("no ssh config")
   149  }
   150  
   151  // Client is a client to the Teleport Proxy SSH server on behalf of a user.
   152  // The Proxy SSH port used to serve only SSH, however portions of the api are
   153  // being migrated to gRPC to reduce latency. The Client is capable of communicating
   154  // to the Proxy via both mechanism; by default it will choose to use gRPC over
   155  // SSH where it is able to.
   156  type Client struct {
   157  	// cfg are the user provided configuration parameters required to
   158  	// connect and interact with the Proxy.
   159  	cfg *ClientConfig
   160  	// grpcConn is the established gRPC connection to the Proxy.
   161  	grpcConn *grpc.ClientConn
   162  	// transport is the transportv1.Client
   163  	transport *transportv1.Client
   164  	// clusterName as determined by inspecting the certificate presented by
   165  	// the Proxy during the connection handshake.
   166  	clusterName *clusterName
   167  }
   168  
   169  // protocolProxySSHGRPC is TLS ALPN protocol value used to indicate gRPC
   170  // traffic intended for the Teleport Proxy on the SSH port.
   171  const protocolProxySSHGRPC string = "teleport-proxy-ssh-grpc"
   172  
   173  // NewClient creates a new Client that attempts to connect to the gRPC
   174  // server being served by the Proxy SSH port by default. If unable to
   175  // connect the Client falls back to connecting to the Proxy SSH port
   176  // via SSH.
   177  //
   178  // If it is known that the gRPC server doesn't serve the required API
   179  // of the caller, then prefer to use NewSSHClient instead which omits
   180  // the gRPC dialing altogether.
   181  func NewClient(ctx context.Context, cfg ClientConfig) (*Client, error) {
   182  	if err := cfg.CheckAndSetDefaults(); err != nil {
   183  		return nil, trace.Wrap(err)
   184  	}
   185  
   186  	clt, err := newGRPCClient(ctx, &cfg)
   187  	if err != nil {
   188  		return nil, trace.Wrap(err)
   189  	}
   190  
   191  	// If connecting via a jump host make a call to perform the
   192  	// TLS handshake to ensure that we get the name of the cluster
   193  	// being connected to from its certificate.
   194  	if cfg.ViaJumpHost {
   195  		if _, err := clt.ClusterDetails(ctx); err != nil {
   196  			return nil, trace.NewAggregate(err, clt.Close())
   197  		}
   198  	}
   199  	return clt, trace.Wrap(err)
   200  }
   201  
   202  // clusterName stores the name of the cluster
   203  // in a protected manner which allows it to
   204  // be set during handshakes with the server.
   205  type clusterName struct {
   206  	name atomic.Pointer[string]
   207  }
   208  
   209  func (c *clusterName) get() string {
   210  	name := c.name.Load()
   211  	if name != nil {
   212  		return *name
   213  	}
   214  	return ""
   215  }
   216  
   217  func (c *clusterName) set(name string) {
   218  	c.name.CompareAndSwap(nil, &name)
   219  }
   220  
   221  // clusterCredentials is a [credentials.TransportCredentials] implementation
   222  // that obtains the name of the cluster being connected to from the certificate
   223  // presented by the server. This allows the client to determine the cluster name when
   224  // connecting via jump hosts.
   225  type clusterCredentials struct {
   226  	credentials.TransportCredentials
   227  	clusterName *clusterName
   228  }
   229  
   230  // teleportClusterASN1ExtensionOID is an extension ID used when encoding/decoding
   231  // origin teleport cluster name into certificates.
   232  var teleportClusterASN1ExtensionOID = asn1.ObjectIdentifier{1, 3, 9999, 1, 7}
   233  
   234  // ClientHandshake performs the handshake with the wrapped [credentials.TransportCredentials] and
   235  // then inspects the provided cert for the [teleportClusterASN1ExtensionOID] to determine
   236  // the cluster that the server belongs to.
   237  func (c *clusterCredentials) ClientHandshake(ctx context.Context, authority string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
   238  	conn, info, err := c.TransportCredentials.ClientHandshake(ctx, authority, conn)
   239  	if err != nil {
   240  		return conn, info, trace.Wrap(err)
   241  	}
   242  
   243  	tlsInfo, ok := info.(credentials.TLSInfo)
   244  	if !ok {
   245  		return conn, info, nil
   246  	}
   247  
   248  	certs := tlsInfo.State.PeerCertificates
   249  	if len(certs) == 0 {
   250  		return conn, info, nil
   251  	}
   252  
   253  	clientCert := certs[0]
   254  	for _, attr := range clientCert.Subject.Names {
   255  		if attr.Type.Equal(teleportClusterASN1ExtensionOID) {
   256  			val, ok := attr.Value.(string)
   257  			if ok {
   258  				c.clusterName.set(val)
   259  				break
   260  			}
   261  		}
   262  	}
   263  
   264  	return conn, info, nil
   265  }
   266  
   267  // newGRPCClient creates a Client that is connected via gRPC.
   268  func newGRPCClient(ctx context.Context, cfg *ClientConfig) (_ *Client, err error) {
   269  	dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
   270  	defer cancel()
   271  
   272  	c := &clusterName{}
   273  
   274  	creds, err := cfg.creds("")
   275  	if err != nil {
   276  		return nil, trace.Wrap(err)
   277  	}
   278  
   279  	conn, err := grpc.DialContext(
   280  		dialCtx,
   281  		cfg.ProxyAddress,
   282  		append([]grpc.DialOption{
   283  			grpc.WithContextDialer(newDialerForGRPCClient(ctx, cfg)),
   284  			grpc.WithTransportCredentials(&clusterCredentials{TransportCredentials: creds, clusterName: c}),
   285  			grpc.WithChainUnaryInterceptor(
   286  				append(cfg.UnaryInterceptors,
   287  					//nolint:staticcheck // SA1019. There is a data race in the stats.Handler that is replacing
   288  					// the interceptor. See https://github.com/open-telemetry/opentelemetry-go-contrib/issues/4576.
   289  					otelgrpc.UnaryClientInterceptor(),
   290  					metadata.UnaryClientInterceptor,
   291  					interceptors.GRPCClientUnaryErrorInterceptor,
   292  				)...,
   293  			),
   294  			grpc.WithChainStreamInterceptor(
   295  				append(cfg.StreamInterceptors,
   296  					//nolint:staticcheck // SA1019. There is a data race in the stats.Handler that is replacing
   297  					// the interceptor. See https://github.com/open-telemetry/opentelemetry-go-contrib/issues/4576.
   298  					otelgrpc.StreamClientInterceptor(),
   299  					metadata.StreamClientInterceptor,
   300  					interceptors.GRPCClientStreamErrorInterceptor,
   301  				)...,
   302  			),
   303  		}, cfg.DialOpts...)...,
   304  	)
   305  	if err != nil {
   306  		return nil, trace.Wrap(err)
   307  	}
   308  
   309  	defer func() {
   310  		if err != nil {
   311  			_ = conn.Close()
   312  		}
   313  	}()
   314  
   315  	transport, err := transportv1.NewClient(transportv1pb.NewTransportServiceClient(conn))
   316  	if err != nil {
   317  		return nil, trace.Wrap(err)
   318  	}
   319  
   320  	return &Client{
   321  		cfg:         cfg,
   322  		grpcConn:    conn,
   323  		transport:   transport,
   324  		clusterName: c,
   325  	}, nil
   326  }
   327  
   328  func newDialerForGRPCClient(ctx context.Context, cfg *ClientConfig) func(context.Context, string) (net.Conn, error) {
   329  	return client.GRPCContextDialer(client.NewDialer(ctx, defaults.DefaultIdleTimeout, cfg.DialTimeout,
   330  		client.WithInsecureSkipVerify(cfg.InsecureSkipVerify),
   331  		client.WithALPNConnUpgrade(cfg.ALPNConnUpgradeRequired),
   332  		client.WithALPNConnUpgradePing(true), // Use Ping protocol for long-lived connections.
   333  		client.WithPROXYHeaderGetter(cfg.PROXYHeaderGetter),
   334  	))
   335  }
   336  
   337  // ClusterName returns the name of the cluster that the
   338  // connected Proxy is a member of.
   339  func (c *Client) ClusterName() string {
   340  	return c.clusterName.get()
   341  }
   342  
   343  // Close attempts to close both the gRPC and SSH connections.
   344  func (c *Client) Close() error {
   345  	return trace.Wrap(c.grpcConn.Close())
   346  }
   347  
   348  // SSHConfig returns the [ssh.ClientConfig] for the provided user which
   349  // should be used when creating a [tracessh.Client] with the returned
   350  // [net.Conn] from [Client.DialHost].
   351  func (c *Client) SSHConfig(user string) *ssh.ClientConfig {
   352  	return &ssh.ClientConfig{
   353  		Config:            c.cfg.SSHConfig.Config,
   354  		User:              user,
   355  		Auth:              c.cfg.SSHConfig.Auth,
   356  		HostKeyCallback:   c.cfg.SSHConfig.HostKeyCallback,
   357  		BannerCallback:    c.cfg.SSHConfig.BannerCallback,
   358  		ClientVersion:     c.cfg.SSHConfig.ClientVersion,
   359  		HostKeyAlgorithms: c.cfg.SSHConfig.HostKeyAlgorithms,
   360  		Timeout:           c.cfg.SSHConfig.Timeout,
   361  	}
   362  }
   363  
   364  // ClusterDetails provide cluster configuration
   365  // details as known by the connected Proxy.
   366  type ClusterDetails struct {
   367  	// FIPS dictates whether FIPS mode is enabled.
   368  	FIPS bool
   369  }
   370  
   371  // ClientConfig returns a [client.Config] that may be used to connect to the
   372  // Auth server in the provided cluster via [client.New] or similar. The [client.Config]
   373  // returned will have the correct credentials and dialer set based on the ClientConfig
   374  // that was provided to create this Client.
   375  func (c *Client) ClientConfig(ctx context.Context, cluster string) (client.Config, error) {
   376  	creds, err := c.cfg.clientCreds(cluster)
   377  	if err != nil {
   378  		return client.Config{}, trace.Wrap(err)
   379  	}
   380  
   381  	if c.cfg.TLSRoutingEnabled {
   382  		return client.Config{
   383  			Context:                    ctx,
   384  			Addrs:                      []string{c.cfg.ProxyAddress},
   385  			Credentials:                []client.Credentials{creds},
   386  			ALPNSNIAuthDialClusterName: cluster,
   387  			CircuitBreakerConfig:       breaker.NoopBreakerConfig(),
   388  			ALPNConnUpgradeRequired:    c.cfg.ALPNConnUpgradeRequired,
   389  			DialOpts:                   c.cfg.DialOpts,
   390  			InsecureAddressDiscovery:   c.cfg.InsecureSkipVerify,
   391  		}, nil
   392  	}
   393  
   394  	return client.Config{
   395  		Context:                  ctx,
   396  		Credentials:              []client.Credentials{creds},
   397  		CircuitBreakerConfig:     breaker.NoopBreakerConfig(),
   398  		DialInBackground:         true,
   399  		InsecureAddressDiscovery: c.cfg.InsecureSkipVerify,
   400  		Dialer: client.ContextDialerFunc(func(dialCtx context.Context, _ string, _ string) (net.Conn, error) {
   401  			conn, err := c.transport.DialCluster(dialCtx, cluster, nil)
   402  			return conn, trace.Wrap(err)
   403  		}),
   404  		DialOpts: c.cfg.DialOpts,
   405  	}, nil
   406  }
   407  
   408  // DialHost establishes a connection to the `target` in cluster named `cluster`. If a keyring
   409  // is provided it will only be forwarded if proxy recording mode is enabled in the cluster.
   410  func (c *Client) DialHost(ctx context.Context, target, cluster string, keyring agent.ExtendedAgent) (net.Conn, ClusterDetails, error) {
   411  	conn, details, err := c.transport.DialHost(ctx, target, cluster, nil, keyring)
   412  	if err != nil {
   413  		return nil, ClusterDetails{}, trace.ConnectionProblem(err, "failed connecting to host %s: %v", target, err)
   414  	}
   415  
   416  	return conn, ClusterDetails{FIPS: details.FipsEnabled}, nil
   417  }
   418  
   419  // ClusterDetails retrieves cluster information as seen by the Proxy.
   420  func (c *Client) ClusterDetails(ctx context.Context) (ClusterDetails, error) {
   421  	details, err := c.transport.ClusterDetails(ctx)
   422  	if err != nil {
   423  		return ClusterDetails{}, trace.Wrap(err)
   424  	}
   425  
   426  	return ClusterDetails{FIPS: details.FipsEnabled}, nil
   427  }
   428  
   429  // Ping measures the round trip latency of sending a message to the Proxy.
   430  func (c *Client) Ping(ctx context.Context) error {
   431  	// TODO(tross): Update to call Ping when it is added to the transport service.
   432  	// For now we don't really care what method is used we just want to measure
   433  	// how long it takes to get a reply. This will always fail with a not implemented
   434  	// error since the Proxy gRPC server doesn't serve the auth service proto. However,
   435  	// we use it because it's already imported in the api package.
   436  	clt := authpb.NewAuthServiceClient(c.grpcConn)
   437  	_, _ = clt.Ping(ctx, &authpb.PingRequest{})
   438  	return nil
   439  }