Remove legacy ALPN connection upgrade support (#63339)

Remove server-side support for legacy (non-WebSocket) ALPN connection
upgrades. Client-side support was removed in v18.0.0 (#52573).
This commit is contained in:
STeve (Xin) Huang
2026-02-02 15:36:25 +00:00
committed by GitHub
parent b88d276dea
commit ebfb6a8c3d
3 changed files with 8 additions and 145 deletions
+4 -10
View File
@@ -489,17 +489,11 @@ const (
// WebAPIConnUpgradeHeader is the header used to indicate the requested
// connection upgrade types in the connection upgrade API.
WebAPIConnUpgradeHeader = "Upgrade"
// WebAPIConnUpgradeTeleportHeader is a Teleport-specific header used to
// indicate the requested connection upgrade types in the connection
// upgrade API. This header is sent in addition to "Upgrade" header in case
// a load balancer/reverse proxy removes "Upgrade".
WebAPIConnUpgradeTeleportHeader = "X-Teleport-Upgrade"
// WebAPIConnUpgradeTypeALPN is a connection upgrade type that specifies
// the upgraded connection should be handled by the ALPN handler.
// WebAPIConnUpgradeTypeALPN is a WebSocket subprotocol identifier for
// ALPN connection upgrades.
WebAPIConnUpgradeTypeALPN = "alpn"
// WebAPIConnUpgradeTypeALPNPing is a connection upgrade type that
// specifies the upgraded connection should be handled by the ALPN handler
// wrapped with the Ping protocol.
// WebAPIConnUpgradeTypeALPNPing is a WebSocket subprotocol identifier for
// ALPN connection upgrades with WebSocket ping frames enabled.
//
// This should be used when the tunneled TLS Routing protocol cannot keep
// long-lived connections alive as L7 LB usually ignores TCP keepalives and
+4 -83
View File
@@ -36,68 +36,19 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/utils/pingconn"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"
logutils "github.com/gravitational/teleport/lib/utils/log"
)
// selectConnectionUpgrade selects the requested upgrade type and returns the
// corresponding handler.
func (h *Handler) selectConnectionUpgrade(r *http.Request) (string, ConnectionHandler, error) {
upgrades := append(
r.Header.Values(constants.WebAPIConnUpgradeTeleportHeader),
r.Header.Values(constants.WebAPIConnUpgradeHeader)...,
)
// Prefer WebSocket when multiple types are provided.
switch {
case slices.Contains(upgrades, constants.WebAPIConnUpgradeTypeWebSocket):
return constants.WebAPIConnUpgradeTypeWebSocket, h.upgradeALPN, nil
case slices.Contains(upgrades, constants.WebAPIConnUpgradeTypeALPNPing):
return constants.WebAPIConnUpgradeTypeALPNPing, h.upgradeALPNWithPing, nil
case slices.Contains(upgrades, constants.WebAPIConnUpgradeTypeALPN):
return constants.WebAPIConnUpgradeTypeALPN, h.upgradeALPN, nil
default:
return "", nil, trace.NotFound("unsupported upgrade types: %v", upgrades)
}
}
// connectionUpgrade handles connection upgrades.
func (h *Handler) connectionUpgrade(w http.ResponseWriter, r *http.Request, p httprouter.Params) (any, error) {
upgradeType, upgradeHandler, err := h.selectConnectionUpgrade(r)
if err != nil {
return nil, trace.Wrap(err)
upgrades := r.Header.Values(constants.WebAPIConnUpgradeHeader)
if !slices.Contains(upgrades, constants.WebAPIConnUpgradeTypeWebSocket) {
return nil, trace.NotFound("unsupported upgrade types: %v", upgrades)
}
if upgradeType == constants.WebAPIConnUpgradeTypeWebSocket {
return h.upgradeALPNWebSocket(w, r, upgradeHandler)
}
// TODO(greedy52) DELETE legacy upgrade in 19.0. Client side is deprecated
// in 18.0.
hj, ok := w.(http.Hijacker)
if !ok {
return nil, trace.BadParameter("failed to hijack connection")
}
conn, _, err := hj.Hijack()
if err != nil {
return nil, trace.Wrap(err)
}
defer conn.Close()
// Since w is hijacked, there is no point returning an error for response
// starting at this point.
if err := writeUpgradeResponse(conn, upgradeType); err != nil {
h.logger.ErrorContext(r.Context(), "Failed to write upgrade response.", "error", err)
return nil, nil
}
if err := upgradeHandler(r.Context(), conn); err != nil && !utils.IsOKNetworkError(err) {
h.logger.ErrorContext(r.Context(), "Failed to handle upgrade request.", "type", upgradeType, "error", err)
}
return nil, nil
return h.upgradeALPNWebSocket(w, r, h.upgradeALPN)
}
func (h *Handler) upgradeALPNWebSocket(w http.ResponseWriter, r *http.Request, upgradeHandler ConnectionHandler) (any, error) {
@@ -165,21 +116,6 @@ func (h *Handler) upgradeALPN(ctx context.Context, conn net.Conn) error {
return h.cfg.ALPNHandler(ctx, waitConn)
}
func (h *Handler) upgradeALPNWithPing(ctx context.Context, conn net.Conn) error {
if h.cfg.ALPNHandler == nil {
return trace.BadParameter("missing ALPNHandler")
}
pingConn := pingconn.New(conn)
// Cancel ping background goroutine when connection is closed.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go h.startPing(ctx, pingConn)
return h.upgradeALPN(ctx, pingConn)
}
type pingWriter interface {
WritePing() error
}
@@ -203,21 +139,6 @@ func (h *Handler) startPing(ctx context.Context, pingConn pingWriter) {
}
}
func writeUpgradeResponse(w io.Writer, upgradeType string) error {
header := make(http.Header)
header.Add(constants.WebAPIConnUpgradeHeader, upgradeType)
header.Add(constants.WebAPIConnUpgradeTeleportHeader, upgradeType)
header.Add(constants.WebAPIConnUpgradeConnectionHeader, constants.WebAPIConnUpgradeConnectionType)
response := &http.Response{
Status: http.StatusText(http.StatusSwitchingProtocols),
StatusCode: http.StatusSwitchingProtocols,
Header: header,
ProtoMajor: 1,
ProtoMinor: 1,
}
return response.Write(w)
}
// waitConn is a net.Conn that provides a "WaitForClose" function to wait until
// the connection is closed.
type waitConn struct {
-52
View File
@@ -20,7 +20,6 @@ package web
import (
"bufio"
"bytes"
"context"
"io"
"log/slog"
@@ -37,26 +36,10 @@ import (
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/utils/pingconn"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/listener"
)
func TestWriteUpgradeResponse(t *testing.T) {
var buf bytes.Buffer
require.NoError(t, writeUpgradeResponse(&buf, "custom"))
resp, err := http.ReadResponse(bufio.NewReader(&buf), nil)
require.NoError(t, err)
// Always drain/close the body.
io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
require.Equal(t, "custom", resp.Header.Get("Upgrade"))
}
func TestHandlerConnectionUpgrade(t *testing.T) {
expectedPayload := "hello@"
expectedIP := "1.2.3.4"
@@ -114,38 +97,6 @@ func TestHandlerConnectionUpgrade(t *testing.T) {
inputRequest: makeConnUpgradeRequest(t, "", "unsupported-protocol", expectedIP),
checkHandlerError: trace.IsNotFound,
},
{
// TODO(greedy52) DELETE in 17.0
name: "upgraded to ALPN (legacy)",
inputALPNHandler: simpleWriteHandler,
inputRequest: makeConnUpgradeRequest(t, "", constants.WebAPIConnUpgradeTypeALPN, expectedIP),
expectUpgradeType: constants.WebAPIConnUpgradeTypeALPN,
checkClientConnString: mustReadClientConnString,
},
{
// TODO(greedy52) DELETE in 17.0
name: "upgraded to ALPN with Ping (legacy)",
inputALPNHandler: simpleWriteHandler,
inputRequest: makeConnUpgradeRequest(t, "", constants.WebAPIConnUpgradeTypeALPNPing, expectedIP),
expectUpgradeType: constants.WebAPIConnUpgradeTypeALPNPing,
wrapClientConn: toNetConn(pingconn.New),
checkClientConnString: mustReadClientConnString,
},
{
// TODO(greedy52) DELETE in 17.0
name: "nested ALPN (legacy) upgrade",
inputALPNHandler: nestedUpgradeHandler,
inputRequest: makeConnUpgradeRequest(t, "", constants.WebAPIConnUpgradeTypeALPN, expectedIP),
expectUpgradeType: constants.WebAPIConnUpgradeTypeALPN,
checkClientConnString: mustWriteNestedWebSocketConnString,
},
{
name: "upgraded to ALPN with Teleport-specific header",
inputALPNHandler: simpleWriteHandler,
inputRequest: makeConnUpgradeRequest(t, constants.WebAPIConnUpgradeTeleportHeader, constants.WebAPIConnUpgradeTypeALPN, expectedIP),
expectUpgradeType: constants.WebAPIConnUpgradeTypeALPN,
checkClientConnString: mustReadClientConnString,
},
{
name: "upgraded to WebSocket",
inputALPNHandler: simpleWriteHandler,
@@ -336,9 +287,6 @@ func mustReadSwitchProtocolsResponse(t *testing.T, r *http.Request, clientConn n
io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
if upgradeType != "websocket" {
require.Equal(t, upgradeType, resp.Header.Get(constants.WebAPIConnUpgradeTeleportHeader))
}
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)