diff --git a/api/client/alpn_conn_upgrade.go b/api/client/alpn_conn_upgrade.go index bbbe4f99a61ae..4ce254df95548 100644 --- a/api/client/alpn_conn_upgrade.go +++ b/api/client/alpn_conn_upgrade.go @@ -195,6 +195,18 @@ func upgradeConnThroughWebAPI(conn net.Conn, api url.URL, upgradeType string) (n req.Header.Add(constants.WebAPIConnUpgradeHeader, upgradeType) + // Set "Connection" header to meet RFC spec: + // https://datatracker.ietf.org/doc/html/rfc2616#section-14.42 + // Quote: "the upgrade keyword MUST be supplied within a Connection header + // field (section 14.10) whenever Upgrade is present in an HTTP/1.1 + // message." + // + // Some L7 load balancers/reverse proxies like "ngrok" and "tailscale" + // require this header to be set to complete the upgrade flow. The header + // must be set on both the upgrade request here and the 101 Switching + // Protocols response from the server. + req.Header.Add(constants.WebAPIConnUpgradeConnectionHeader, constants.WebAPIConnUpgradeConnectionType) + // Send the request and check if upgrade is successful. if err = req.Write(conn); err != nil { return nil, trace.Wrap(err) diff --git a/api/client/alpn_conn_upgrade_test.go b/api/client/alpn_conn_upgrade_test.go index a52a412134a8a..56a5bce36a2c1 100644 --- a/api/client/alpn_conn_upgrade_test.go +++ b/api/client/alpn_conn_upgrade_test.go @@ -240,6 +240,7 @@ func mockConnUpgradeHandler(t *testing.T, upgradeType string, write []byte) http return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, constants.WebAPIConnUpgrade, r.URL.Path) require.Equal(t, upgradeType, r.Header.Get(constants.WebAPIConnUpgradeHeader)) + require.Equal(t, constants.WebAPIConnUpgradeConnectionType, r.Header.Get(constants.WebAPIConnUpgradeConnectionHeader)) hj, ok := w.(http.Hijacker) require.True(t, ok) diff --git a/api/constants/constants.go b/api/constants/constants.go index e886e11aa5269..c2557ef4e7fe7 100644 --- a/api/constants/constants.go +++ b/api/constants/constants.go @@ -413,4 +413,11 @@ const ( // long-lived connections alive as L7 LB usually ignores TCP keepalives and // has very short idle timeouts. WebAPIConnUpgradeTypeALPNPing = "alpn-ping" + // WebAPIConnUpgradeConnectionHeader is the standard header that controls + // whether the network connection stays open after the current transaction + // finishes. + WebAPIConnUpgradeConnectionHeader = "Connection" + // WebAPIConnUpgradeConnectionType is the value of the "Connection" header + // used for connection upgrades. + WebAPIConnUpgradeConnectionType = "Upgrade" ) diff --git a/lib/web/conn_upgrade.go b/lib/web/conn_upgrade.go index fdb03332b041a..e4523fecef7d5 100644 --- a/lib/web/conn_upgrade.go +++ b/lib/web/conn_upgrade.go @@ -130,6 +130,7 @@ func (h *Handler) startPing(ctx context.Context, pingConn *pingconn.PingConn) { func writeUpgradeResponse(w io.Writer, upgradeType string) error { header := make(http.Header) header.Add(constants.WebAPIConnUpgradeHeader, upgradeType) + header.Add(constants.WebAPIConnUpgradeConnectionHeader, constants.WebAPIConnUpgradeConnectionType) response := &http.Response{ Status: http.StatusText(http.StatusSwitchingProtocols), StatusCode: http.StatusSwitchingProtocols, diff --git a/lib/web/conn_upgrade_test.go b/lib/web/conn_upgrade_test.go index 76b566beeeace..7828c47a9dafd 100644 --- a/lib/web/conn_upgrade_test.go +++ b/lib/web/conn_upgrade_test.go @@ -133,6 +133,8 @@ func sendConnUpgradeRequest(t *testing.T, h *Handler, upgradeType string, server io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() + require.Equal(t, upgradeType, resp.Header.Get(constants.WebAPIConnUpgradeHeader)) + require.Equal(t, constants.WebAPIConnUpgradeConnectionType, resp.Header.Get(constants.WebAPIConnUpgradeConnectionHeader)) require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode) }