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

     1  /*
     2  Copyright 2022 Gravitational, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package client
    18  
    19  import (
    20  	"bufio"
    21  	"context"
    22  	"crypto/tls"
    23  	"errors"
    24  	"log/slog"
    25  	"net"
    26  	"net/http"
    27  	"net/url"
    28  	"os"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/gravitational/trace"
    33  
    34  	"github.com/gravitational/teleport/api/constants"
    35  	"github.com/gravitational/teleport/api/defaults"
    36  	"github.com/gravitational/teleport/api/utils"
    37  	"github.com/gravitational/teleport/api/utils/pingconn"
    38  	"github.com/gravitational/teleport/api/utils/tlsutils"
    39  )
    40  
    41  // IsALPNConnUpgradeRequired returns true if a tunnel is required through a HTTP
    42  // connection upgrade for ALPN connections.
    43  //
    44  // The function makes a test connection to the Proxy Service and checks if the
    45  // ALPN is supported. If not, the Proxy Service is likely behind an AWS ALB or
    46  // some custom proxy services that strip out ALPN and SNI information on the
    47  // way to our Proxy Service.
    48  //
    49  // In those cases, the Teleport client should make a HTTP "upgrade" call to the
    50  // Proxy Service to establish a tunnel for the originally planned traffic to
    51  // preserve the ALPN and SNI information.
    52  func IsALPNConnUpgradeRequired(ctx context.Context, addr string, insecure bool, opts ...DialOption) bool {
    53  	if result, ok := OverwriteALPNConnUpgradeRequirementByEnv(addr); ok {
    54  		return result
    55  	}
    56  
    57  	// Use NewDialer which takes care of ProxyURL, and use a shorter I/O
    58  	// timeout to avoid blocking caller.
    59  	baseDialer := NewDialer(
    60  		ctx,
    61  		defaults.DefaultIdleTimeout,
    62  		5*time.Second,
    63  		append(opts,
    64  			WithInsecureSkipVerify(insecure),
    65  			WithALPNConnUpgrade(false),
    66  		)...,
    67  	)
    68  
    69  	tlsConfig := &tls.Config{
    70  		NextProtos:         []string{string(constants.ALPNSNIProtocolReverseTunnel)},
    71  		InsecureSkipVerify: insecure,
    72  	}
    73  	testConn, err := tlsutils.TLSDial(ctx, baseDialer, "tcp", addr, tlsConfig)
    74  	logger := slog.With("address", addr)
    75  	if err != nil {
    76  		if isRemoteNoALPNError(err) {
    77  			logger.DebugContext(ctx, "No ALPN protocol is negotiated by the server.", "upgrade_required", true)
    78  			return true
    79  		}
    80  		if isUnadvertisedALPNError(err) {
    81  			logger.DebugContext(ctx, "ALPN connection upgrade received an unadvertised ALPN protocol.", "error", err)
    82  			return true
    83  		}
    84  
    85  		// If dialing TLS fails for any other reason, we assume connection
    86  		// upgrade is not required so it will fallback to original connection
    87  		// method.
    88  		logger.InfoContext(ctx, "ALPN connection upgrade test failed.", "error", err)
    89  		return false
    90  	}
    91  	defer testConn.Close()
    92  
    93  	// Upgrade required when ALPN is not supported on the remote side so
    94  	// NegotiatedProtocol comes back as empty.
    95  	result := testConn.ConnectionState().NegotiatedProtocol == ""
    96  	logger.DebugContext(ctx, "ALPN connection upgrade test complete", "upgrade_required", result)
    97  	return result
    98  }
    99  
   100  func isRemoteNoALPNError(err error) bool {
   101  	var opErr *net.OpError
   102  	return errors.As(err, &opErr) && opErr.Op == "remote error" && strings.Contains(opErr.Err.Error(), "tls: no application protocol")
   103  }
   104  
   105  // isUnadvertisedALPNError returns true if the error indicates that the server
   106  // returns an ALPN value that the client does not expect during TLS handshake.
   107  //
   108  // Reference:
   109  // https://github.com/golang/go/blob/2639a17f146cc7df0778298c6039156d7ca68202/src/crypto/tls/handshake_client.go#L838
   110  func isUnadvertisedALPNError(err error) bool {
   111  	return strings.Contains(err.Error(), "tls: server selected unadvertised ALPN protocol")
   112  }
   113  
   114  // OverwriteALPNConnUpgradeRequirementByEnv overwrites ALPN connection upgrade
   115  // requirement by environment variable.
   116  //
   117  // TODO(greedy52) DELETE in ??. Note that this toggle was planned to be deleted
   118  // in 15.0 when the feature exits preview. However, many users still rely on
   119  // this manual toggle as IsALPNConnUpgradeRequired cannot detect many
   120  // situations where connection upgrade is required. This can be deleted once
   121  // IsALPNConnUpgradeRequired is improved.
   122  func OverwriteALPNConnUpgradeRequirementByEnv(addr string) (bool, bool) {
   123  	envValue := os.Getenv(defaults.TLSRoutingConnUpgradeEnvVar)
   124  	if envValue == "" {
   125  		return false, false
   126  	}
   127  	result := isALPNConnUpgradeRequiredByEnv(addr, envValue)
   128  	slog.DebugContext(context.TODO(), "Determining if ALPN connection upgrade is explicitly forced due to environment variables.", defaults.TLSRoutingConnUpgradeEnvVar, envValue, "address", addr, "upgrade_required", result)
   129  	return result, true
   130  }
   131  
   132  // isALPNConnUpgradeRequiredByEnv checks if ALPN connection upgrade is required
   133  // based on provided env value.
   134  //
   135  // The env value should contain a list of conditions separated by either ';' or
   136  // ','. A condition is in format of either '<addr>=<bool>' or '<bool>'. The
   137  // former specifies the upgrade requirement for a specific address and the
   138  // later specifies the upgrade requirement for all other addresses. By default,
   139  // upgrade is not required if target is not specified in the env value.
   140  //
   141  // Sample values:
   142  // true
   143  // <some.cluster.com>=yes,<another.cluster.com>=no
   144  // 0,<some.cluster.com>=1
   145  func isALPNConnUpgradeRequiredByEnv(addr, envValue string) bool {
   146  	tokens := strings.FieldsFunc(envValue, func(r rune) bool {
   147  		return r == ';' || r == ','
   148  	})
   149  
   150  	var upgradeRequiredForAll bool
   151  	for _, token := range tokens {
   152  		switch {
   153  		case strings.ContainsRune(token, '='):
   154  			if _, boolText, ok := strings.Cut(token, addr+"="); ok {
   155  				upgradeRequiredForAddr, err := utils.ParseBool(boolText)
   156  				if err != nil {
   157  					slog.DebugContext(context.TODO(), "Failed to parse ALPN connection upgrade environment variable", "value", envValue, "error", err)
   158  				}
   159  				return upgradeRequiredForAddr
   160  			}
   161  
   162  		default:
   163  			if boolValue, err := utils.ParseBool(token); err != nil {
   164  				slog.DebugContext(context.TODO(), "Failed to parse ALPN connection upgrade environment variable", "value", envValue, "error", err)
   165  			} else {
   166  				upgradeRequiredForAll = boolValue
   167  			}
   168  		}
   169  	}
   170  	return upgradeRequiredForAll
   171  }
   172  
   173  // alpnConnUpgradeDialer makes an "HTTP" upgrade call to the Proxy Service then
   174  // tunnels the connection with this connection upgrade.
   175  type alpnConnUpgradeDialer struct {
   176  	dialer    ContextDialer
   177  	tlsConfig *tls.Config
   178  	withPing  bool
   179  }
   180  
   181  // newALPNConnUpgradeDialer creates a new alpnConnUpgradeDialer.
   182  func newALPNConnUpgradeDialer(dialer ContextDialer, tlsConfig *tls.Config, withPing bool) ContextDialer {
   183  	return &alpnConnUpgradeDialer{
   184  		dialer:    dialer,
   185  		tlsConfig: tlsConfig,
   186  		withPing:  withPing,
   187  	}
   188  }
   189  
   190  // DialContext implements ContextDialer
   191  func (d *alpnConnUpgradeDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
   192  	tlsConn, err := tlsutils.TLSDial(ctx, d.dialer, network, addr, d.tlsConfig.Clone())
   193  	if err != nil {
   194  		return nil, trace.Wrap(err)
   195  	}
   196  	upgradeURL := url.URL{
   197  		Host:   addr,
   198  		Scheme: "https",
   199  		Path:   constants.WebAPIConnUpgrade,
   200  	}
   201  
   202  	conn, err := upgradeConnThroughWebAPI(tlsConn, upgradeURL, d.upgradeType())
   203  	if err != nil {
   204  		return nil, trace.NewAggregate(tlsConn.Close(), err)
   205  	}
   206  	return conn, nil
   207  }
   208  
   209  func (d *alpnConnUpgradeDialer) upgradeType() string {
   210  	if d.withPing {
   211  		return constants.WebAPIConnUpgradeTypeALPNPing
   212  	}
   213  	return constants.WebAPIConnUpgradeTypeALPN
   214  }
   215  
   216  func upgradeConnThroughWebAPI(conn net.Conn, api url.URL, alpnUpgradeType string) (net.Conn, error) {
   217  	req, err := http.NewRequest(http.MethodGet, api.String(), nil)
   218  	if err != nil {
   219  		return nil, trace.Wrap(err)
   220  	}
   221  
   222  	challengeKey, err := generateWebSocketChallengeKey()
   223  	if err != nil {
   224  		return nil, trace.Wrap(err)
   225  	}
   226  
   227  	// Prefer "websocket".
   228  	if useConnUpgradeMode.useWebSocket() {
   229  		applyWebSocketUpgradeHeaders(req, alpnUpgradeType, challengeKey)
   230  	}
   231  
   232  	// Append "legacy" custom upgrade type.
   233  	// TODO(greedy52) DELETE in 17.0
   234  	if useConnUpgradeMode.useLegacy() {
   235  		req.Header.Add(constants.WebAPIConnUpgradeHeader, alpnUpgradeType)
   236  		req.Header.Add(constants.WebAPIConnUpgradeTeleportHeader, alpnUpgradeType)
   237  	}
   238  
   239  	// Set "Connection" header to meet RFC spec:
   240  	// https://datatracker.ietf.org/doc/html/rfc2616#section-14.42
   241  	// Quote: "the upgrade keyword MUST be supplied within a Connection header
   242  	// field (section 14.10) whenever Upgrade is present in an HTTP/1.1
   243  	// message."
   244  	//
   245  	// Some L7 load balancers/reverse proxies like "ngrok" and "tailscale"
   246  	// require this header to be set to complete the upgrade flow. The header
   247  	// must be set on both the upgrade request here and the 101 Switching
   248  	// Protocols response from the server.
   249  	req.Header.Set(constants.WebAPIConnUpgradeConnectionHeader, constants.WebAPIConnUpgradeConnectionType)
   250  
   251  	// Send the request and check if upgrade is successful.
   252  	if err = req.Write(conn); err != nil {
   253  		return nil, trace.Wrap(err)
   254  	}
   255  	resp, err := http.ReadResponse(bufio.NewReader(conn), req)
   256  	if err != nil {
   257  		return nil, trace.Wrap(err)
   258  	}
   259  	defer resp.Body.Close()
   260  
   261  	if http.StatusSwitchingProtocols != resp.StatusCode {
   262  		if http.StatusNotFound == resp.StatusCode {
   263  			return nil, trace.NotImplemented(
   264  				"connection upgrade call to %q with upgrade type %v failed with status code %v. Please upgrade the server and try again.",
   265  				constants.WebAPIConnUpgrade,
   266  				alpnUpgradeType,
   267  				resp.StatusCode,
   268  			)
   269  		}
   270  		return nil, trace.BadParameter("failed to switch Protocols %v", resp.StatusCode)
   271  	}
   272  
   273  	// Handle WebSocket.
   274  	logger := slog.With("hostname", api.Host)
   275  	if resp.Header.Get(constants.WebAPIConnUpgradeHeader) == constants.WebAPIConnUpgradeTypeWebSocket {
   276  		if err := checkWebSocketUpgradeResponse(resp, alpnUpgradeType, challengeKey); err != nil {
   277  			return nil, trace.Wrap(err)
   278  		}
   279  
   280  		logger.DebugContext(req.Context(), "Performing ALPN WebSocket connection upgrade.")
   281  		return newWebSocketALPNClientConn(conn), nil
   282  	}
   283  
   284  	// Handle "legacy".
   285  	// TODO(greedy52) DELETE in 17.0.
   286  	logger.DebugContext(req.Context(), "Performing ALPN legacy connection upgrade.")
   287  	if alpnUpgradeType == constants.WebAPIConnUpgradeTypeALPNPing {
   288  		return pingconn.New(conn), nil
   289  	}
   290  	return conn, nil
   291  }
   292  
   293  type connUpgradeMode string
   294  
   295  func (m connUpgradeMode) useWebSocket() bool {
   296  	// Use WebSocket as long as it's not legacy only.
   297  	return strings.ToLower(string(m)) != "legacy"
   298  }
   299  
   300  func (m connUpgradeMode) useLegacy() bool {
   301  	// Use legacy as long as it's not WebSocket only.
   302  	return strings.ToLower(string(m)) != "websocket"
   303  }
   304  
   305  var (
   306  	useConnUpgradeMode connUpgradeMode = connUpgradeMode(os.Getenv(defaults.TLSRoutingConnUpgradeModeEnvVar))
   307  )