Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions api/client/alpn_conn_upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions api/client/alpn_conn_upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions api/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
1 change: 1 addition & 0 deletions lib/web/conn_upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions lib/web/conn_upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down