Skip to content

Commit eb9cf77

Browse files
Disable HTTP2 while proxying a "Connection: upgrade" request
When proxying connection upgrade requests, like websockets, we dial the target and then manually write the http.Request to the wire, bypassing the http.Client. In this scenario we are by default using HTTP/1.1 from both the client and to the target server we are proxying. Because of this we must disable HTTP2 in the TLS handshake so that the server does not think we are writing a HTTP2 request. We do this by setting the TLSConfig.NextProtos field to "http/1.1". Signed-off-by: Darren Shepherd <[email protected]>
1 parent 861c918 commit eb9cf77

File tree

4 files changed

+73
-10
lines changed

4 files changed

+73
-10
lines changed

staging/src/k8s.io/apimachinery/pkg/util/proxy/dial.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ import (
3030
"k8s.io/apimachinery/third_party/forked/golang/netutil"
3131
)
3232

33-
func DialURL(ctx context.Context, url *url.URL, transport http.RoundTripper) (net.Conn, error) {
33+
// dialURL will dial the specified URL using the underlying dialer held by the passed
34+
// RoundTripper. The primary use of this method is to support proxying upgradable connections.
35+
// For this reason this method will prefer to negotiate http/1.1 if the URL scheme is https.
36+
// If you wish to ensure ALPN negotiates http2 then set NextProto=[]string{"http2"} in the
37+
// TLSConfig of the http.Transport
38+
func dialURL(ctx context.Context, url *url.URL, transport http.RoundTripper) (net.Conn, error) {
3439
dialAddr := netutil.CanonicalAddr(url)
3540

3641
dialer, err := utilnet.DialerFor(transport)
@@ -81,6 +86,15 @@ func DialURL(ctx context.Context, url *url.URL, transport http.RoundTripper) (ne
8186
tlsConfigCopy.ServerName = inferredHost
8287
tlsConfig = tlsConfigCopy
8388
}
89+
90+
// Since this method is primary used within a "Connection: Upgrade" call we assume the caller is
91+
// going to write HTTP/1.1 request to the wire. http2 should not be allowed in the TLSConfig.NextProtos,
92+
// so we explicitly set that here. We only do this check if the TLSConfig support http/1.1.
93+
if supportsHTTP11(tlsConfig.NextProtos) {
94+
tlsConfig = tlsConfig.Clone()
95+
tlsConfig.NextProtos = []string{"http/1.1"}
96+
}
97+
8498
tlsConn = tls.Client(netConn, tlsConfig)
8599
if err := tlsConn.Handshake(); err != nil {
86100
netConn.Close()
@@ -115,3 +129,15 @@ func DialURL(ctx context.Context, url *url.URL, transport http.RoundTripper) (ne
115129
return nil, fmt.Errorf("Unknown scheme: %s", url.Scheme)
116130
}
117131
}
132+
133+
func supportsHTTP11(nextProtos []string) bool {
134+
if len(nextProtos) == 0 {
135+
return true
136+
}
137+
for _, proto := range nextProtos {
138+
if proto == "http/1.1" {
139+
return true
140+
}
141+
}
142+
return false
143+
}

staging/src/k8s.io/apimachinery/pkg/util/proxy/dial_test.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func TestDialURL(t *testing.T) {
4949
TLSConfig *tls.Config
5050
Dial utilnet.DialFunc
5151
ExpectError string
52+
ExpectProto string
5253
}{
5354
"insecure": {
5455
TLSConfig: &tls.Config{InsecureSkipVerify: true},
@@ -90,13 +91,28 @@ func TestDialURL(t *testing.T) {
9091
TLSConfig: &tls.Config{InsecureSkipVerify: false, RootCAs: roots, ServerName: "example.com"},
9192
Dial: d.DialContext,
9293
},
94+
"ensure we use http2 if specified": {
95+
TLSConfig: &tls.Config{InsecureSkipVerify: false, RootCAs: roots, ServerName: "example.com", NextProtos: []string{"http2"}},
96+
Dial: d.DialContext,
97+
ExpectProto: "http2",
98+
},
99+
"ensure we use http/1.1 if unspecified": {
100+
TLSConfig: &tls.Config{InsecureSkipVerify: false, RootCAs: roots, ServerName: "example.com"},
101+
Dial: d.DialContext,
102+
ExpectProto: "http/1.1",
103+
},
104+
"ensure we use http/1.1 if available": {
105+
TLSConfig: &tls.Config{InsecureSkipVerify: false, RootCAs: roots, ServerName: "example.com", NextProtos: []string{"http2", "http/1.1"}},
106+
Dial: d.DialContext,
107+
ExpectProto: "http/1.1",
108+
},
93109
}
94110

95111
for k, tc := range testcases {
96112
func() {
97113
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {}))
98114
defer ts.Close()
99-
ts.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
115+
ts.TLS = &tls.Config{Certificates: []tls.Certificate{cert}, NextProtos: []string{"http2", "http/1.1"}}
100116
ts.StartTLS()
101117

102118
// Make a copy of the config
@@ -127,7 +143,7 @@ func TestDialURL(t *testing.T) {
127143
u, _ := url.Parse(ts.URL)
128144
_, p, _ := net.SplitHostPort(u.Host)
129145
u.Host = net.JoinHostPort("127.0.0.1", p)
130-
conn, err := DialURL(context.Background(), u, transport)
146+
conn, err := dialURL(context.Background(), u, transport)
131147

132148
// Make sure dialing doesn't mutate the transport's TLSConfig
133149
if !reflect.DeepEqual(tc.TLSConfig, tlsConfigCopy) {
@@ -143,6 +159,14 @@ func TestDialURL(t *testing.T) {
143159
}
144160
return
145161
}
162+
163+
tlsConn := conn.(*tls.Conn)
164+
if tc.ExpectProto != "" {
165+
if tlsConn.ConnectionState().NegotiatedProtocol != tc.ExpectProto {
166+
t.Errorf("%s: expected proto %s, got %s", k, tc.ExpectProto, tlsConn.ConnectionState().NegotiatedProtocol)
167+
}
168+
}
169+
146170
conn.Close()
147171
if tc.ExpectError != "" {
148172
t.Errorf("%s: expected error %q, got none", k, tc.ExpectError)

staging/src/k8s.io/apimachinery/pkg/util/proxy/upgradeaware.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -384,10 +384,6 @@ func (h *UpgradeAwareHandler) tryUpgrade(w http.ResponseWriter, req *http.Reques
384384
return true
385385
}
386386

387-
func (h *UpgradeAwareHandler) Dial(req *http.Request) (net.Conn, error) {
388-
return dial(req, h.Transport)
389-
}
390-
391387
func (h *UpgradeAwareHandler) DialForUpgrade(req *http.Request) (net.Conn, error) {
392388
if h.UpgradeTransport == nil {
393389
return dial(req, h.Transport)
@@ -414,7 +410,7 @@ func getResponse(r io.Reader) (*http.Response, []byte, error) {
414410

415411
// dial dials the backend at req.URL and writes req to it.
416412
func dial(req *http.Request, transport http.RoundTripper) (net.Conn, error) {
417-
conn, err := DialURL(req.Context(), req.URL, transport)
413+
conn, err := dialURL(req.Context(), req.URL, transport)
418414
if err != nil {
419415
return nil, fmt.Errorf("error dialing backend: %v", err)
420416
}
@@ -427,8 +423,6 @@ func dial(req *http.Request, transport http.RoundTripper) (net.Conn, error) {
427423
return conn, err
428424
}
429425

430-
var _ utilnet.Dialer = &UpgradeAwareHandler{}
431-
432426
func (h *UpgradeAwareHandler) defaultProxyTransport(url *url.URL, internalTransport http.RoundTripper) http.RoundTripper {
433427
scheme := url.Scheme
434428
host := url.Host

staging/src/k8s.io/apimachinery/pkg/util/proxy/upgradeaware_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,25 @@ func TestProxyUpgrade(t *testing.T) {
355355
ServerFunc: httptest.NewServer,
356356
ProxyTransport: nil,
357357
},
358+
"both client and server support http2, but force to http/1.1 for upgrade": {
359+
ServerFunc: func(h http.Handler) *httptest.Server {
360+
cert, err := tls.X509KeyPair(exampleCert, exampleKey)
361+
if err != nil {
362+
t.Errorf("https (invalid hostname): proxy_test: %v", err)
363+
}
364+
ts := httptest.NewUnstartedServer(h)
365+
ts.TLS = &tls.Config{
366+
Certificates: []tls.Certificate{cert},
367+
NextProtos: []string{"http2", "http/1.1"},
368+
}
369+
ts.StartTLS()
370+
return ts
371+
},
372+
ProxyTransport: utilnet.SetTransportDefaults(&http.Transport{TLSClientConfig: &tls.Config{
373+
NextProtos: []string{"http2", "http/1.1"},
374+
InsecureSkipVerify: true,
375+
}}),
376+
},
358377
"https (invalid hostname + InsecureSkipVerify)": {
359378
ServerFunc: func(h http.Handler) *httptest.Server {
360379
cert, err := tls.X509KeyPair(exampleCert, exampleKey)

0 commit comments

Comments
 (0)