Skip to content

Commit 7c7ce47

Browse files
authored
Merge pull request kubernetes#88781 from ibuildthecloud/master
Disable HTTP2 while proxying a "Connection: upgrade" request
2 parents 61847ea + eb9cf77 commit 7c7ce47

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)