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 )