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 }