Skip to content

Commit b6e8438

Browse files
seans3k8s-publishing-bot
authored andcommitted
Falls back to SPDY for gorilla/websocket https proxy error
Kubernetes-commit: 6450174ac9a07f15aecfa5f05d58755efc2192b7
1 parent 4e1652b commit b6e8438

File tree

3 files changed

+228
-2
lines changed

3 files changed

+228
-2
lines changed

Diff for: tools/portforward/fallback_dialer_test.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ limitations under the License.
1717
package portforward
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"testing"
2223

2324
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
2426
"k8s.io/apimachinery/pkg/util/httpstream"
2527
)
2628

@@ -36,7 +38,7 @@ func TestFallbackDialer(t *testing.T) {
3638
assert.True(t, primary.dialed, "no fallback; primary should have dialed")
3739
assert.False(t, secondary.dialed, "no fallback; secondary should *not* have dialed")
3840
assert.Equal(t, primaryProtocol, negotiated, "primary negotiated protocol returned")
39-
assert.Nil(t, err, "error from primary dialer should be nil")
41+
require.NoError(t, err, "error from primary dialer should be nil")
4042
// If primary dialer error is upgrade error, then fallback returning secondary dial response.
4143
primary = &fakeDialer{dialed: false, negotiatedProtocol: primaryProtocol, err: &httpstream.UpgradeFailureError{}}
4244
secondary = &fakeDialer{dialed: false, negotiatedProtocol: secondaryProtocol}
@@ -45,7 +47,18 @@ func TestFallbackDialer(t *testing.T) {
4547
assert.True(t, primary.dialed, "fallback; primary should have dialed")
4648
assert.True(t, secondary.dialed, "fallback; secondary should have dialed")
4749
assert.Equal(t, secondaryProtocol, negotiated, "negotiated protocol is from secondary dialer")
48-
assert.Nil(t, err, "error from secondary dialer should be nil")
50+
require.NoError(t, err, "error from secondary dialer should be nil")
51+
// If primary dialer error is https proxy dialing error, then fallback returning secondary dial response.
52+
primary = &fakeDialer{negotiatedProtocol: primaryProtocol, err: errors.New("proxy: unknown scheme: https")}
53+
secondary = &fakeDialer{negotiatedProtocol: secondaryProtocol}
54+
fallbackDialer = NewFallbackDialer(primary, secondary, func(err error) bool {
55+
return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
56+
})
57+
_, negotiated, err = fallbackDialer.Dial(protocols...)
58+
assert.True(t, primary.dialed, "fallback; primary should have dialed")
59+
assert.True(t, secondary.dialed, "fallback; secondary should have dialed")
60+
assert.Equal(t, secondaryProtocol, negotiated, "negotiated protocol is from secondary dialer")
61+
require.NoError(t, err, "error from secondary dialer should be nil")
4962
// If primary dialer returns non-upgrade error, then primary error is returned.
5063
nonUpgradeErr := fmt.Errorf("This is a non-upgrade error")
5164
primary = &fakeDialer{dialed: false, err: nonUpgradeErr}

Diff for: tools/remotecommand/fallback_test.go

+176
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,19 @@ import (
2020
"bytes"
2121
"context"
2222
"crypto/rand"
23+
"crypto/tls"
2324
"io"
2425
"net/http"
2526
"net/http/httptest"
2627
"net/url"
28+
"sync/atomic"
2729
"testing"
2830
"time"
2931

3032
"github.com/stretchr/testify/assert"
3133
"github.com/stretchr/testify/require"
34+
"k8s.io/apimachinery/pkg/util/httpstream"
35+
utilnettesting "k8s.io/apimachinery/pkg/util/net/testing"
3236
"k8s.io/apimachinery/pkg/util/remotecommand"
3337
"k8s.io/apimachinery/pkg/util/wait"
3438
"k8s.io/client-go/rest"
@@ -165,6 +169,178 @@ func TestFallbackClient_SPDYSecondarySucceeds(t *testing.T) {
165169
}
166170
}
167171

172+
// localhostCert was generated from crypto/tls/generate_cert.go with the following command:
173+
//
174+
// go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
175+
var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
176+
MIIDGTCCAgGgAwIBAgIRALL5AZcefF4kkYV1SEG6YrMwDQYJKoZIhvcNAQELBQAw
177+
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
178+
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEP
179+
ADCCAQoCggEBALQ/FHcyVwdFHxARbbD2KBtDUT7Eni+8ioNdjtGcmtXqBv45EC1C
180+
JOqqGJTroFGJ6Q9kQIZ9FqH5IJR2fOOJD9kOTueG4Vt1JY1rj1Kbpjefu8XleZ5L
181+
SBwIWVnN/lEsEbuKmj7N2gLt5AH3zMZiBI1mg1u9Z5ZZHYbCiTpBrwsq6cTlvR9g
182+
dyo1YkM5hRESCzsrL0aUByoo0qRMD8ZsgANJwgsiO0/M6idbxDwv1BnGwGmRYvOE
183+
Hxpy3v0Jg7GJYrvnpnifJTs4nw91N5X9pXxR7FFzi/6HTYDWRljvTb0w6XciKYAz
184+
bWZ0+cJr5F7wB7ovlbm7HrQIR7z7EIIu2d8CAwEAAaNoMGYwDgYDVR0PAQH/BAQD
185+
AgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMBAf8wLgYDVR0R
186+
BCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZI
187+
hvcNAQELBQADggEBAFPPWopNEJtIA2VFAQcqN6uJK+JVFOnjGRoCrM6Xgzdm0wxY
188+
XCGjsxY5dl+V7KzdGqu858rCaq5osEBqypBpYAnS9C38VyCDA1vPS1PsN8SYv48z
189+
DyBwj+7R2qar0ADBhnhWxvYO9M72lN/wuCqFKYMeFSnJdQLv3AsrrHe9lYqOa36s
190+
8wxSwVTFTYXBzljPEnSaaJMPqFD8JXaZK1ryJPkO5OsCNQNGtatNiWAf3DcmwHAT
191+
MGYMzP0u4nw47aRz9shB8w+taPKHx2BVwE1m/yp3nHVioOjXqA1fwRQVGclCJSH1
192+
D2iq3hWVHRENgjTjANBPICLo9AZ4JfN6PH19mnU=
193+
-----END CERTIFICATE-----`)
194+
195+
// localhostKey is the private key for localhostCert.
196+
var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
197+
MIIEogIBAAKCAQEAtD8UdzJXB0UfEBFtsPYoG0NRPsSeL7yKg12O0Zya1eoG/jkQ
198+
LUIk6qoYlOugUYnpD2RAhn0WofkglHZ844kP2Q5O54bhW3UljWuPUpumN5+7xeV5
199+
nktIHAhZWc3+USwRu4qaPs3aAu3kAffMxmIEjWaDW71nllkdhsKJOkGvCyrpxOW9
200+
H2B3KjViQzmFERILOysvRpQHKijSpEwPxmyAA0nCCyI7T8zqJ1vEPC/UGcbAaZFi
201+
84QfGnLe/QmDsYliu+emeJ8lOzifD3U3lf2lfFHsUXOL/odNgNZGWO9NvTDpdyIp
202+
gDNtZnT5wmvkXvAHui+VubsetAhHvPsQgi7Z3wIDAQABAoIBAGmw93IxjYCQ0ncc
203+
kSKMJNZfsdtJdaxuNRZ0nNNirhQzR2h403iGaZlEpmdkhzxozsWcto1l+gh+SdFk
204+
bTUK4MUZM8FlgO2dEqkLYh5BcMT7ICMZvSfJ4v21E5eqR68XVUqQKoQbNvQyxFk3
205+
EddeEGdNrkb0GDK8DKlBlzAW5ep4gjG85wSTjR+J+muUv3R0BgLBFSuQnIDM/IMB
206+
LWqsja/QbtB7yppe7jL5u8UCFdZG8BBKT9fcvFIu5PRLO3MO0uOI7LTc8+W1Xm23
207+
uv+j3SY0+v+6POjK0UlJFFi/wkSPTFIfrQO1qFBkTDQHhQ6q/7GnILYYOiGbIRg2
208+
NNuP52ECgYEAzXEoy50wSYh8xfFaBuxbm3ruuG2W49jgop7ZfoFrPWwOQKAZS441
209+
VIwV4+e5IcA6KkuYbtGSdTYqK1SMkgnUyD/VevwAqH5TJoEIGu0pDuKGwVuwqioZ
210+
frCIAV5GllKyUJ55VZNbRr2vY2fCsWbaCSCHETn6C16DNuTCe5C0JBECgYEA4JqY
211+
5GpNbMG8fOt4H7hU0Fbm2yd6SHJcQ3/9iimef7xG6ajxsYrIhg1ft+3IPHMjVI0+
212+
9brwHDnWg4bOOx/VO4VJBt6Dm/F33bndnZRkuIjfSNpLM51P+EnRdaFVHOJHwKqx
213+
uF69kihifCAG7YATgCveeXImzBUSyZUz9UrETu8CgYARNBimdFNG1RcdvEg9rC0/
214+
p9u1tfecvNySwZqU7WF9kz7eSonTueTdX521qAHowaAdSpdJMGODTTXaywm6cPhQ
215+
jIfj9JZZhbqQzt1O4+08Qdvm9TamCUB5S28YLjza+bHU7nBaqixKkDfPqzCyilpX
216+
yVGGL8SwjwmN3zop/sQXAQKBgC0JMsESQ6YcDsRpnrOVjYQc+LtW5iEitTdfsaID
217+
iGGKihmOI7B66IxgoCHMTws39wycKdSyADVYr5e97xpR3rrJlgQHmBIrz+Iow7Q2
218+
LiAGaec8xjl6QK/DdXmFuQBKqyKJ14rljFODP4QuE9WJid94bGqjpf3j99ltznZP
219+
4J8HAoGAJb4eb4lu4UGwifDzqfAPzLGCoi0fE1/hSx34lfuLcc1G+LEu9YDKoOVJ
220+
9suOh0b5K/bfEy9KrVMBBriduvdaERSD8S3pkIQaitIz0B029AbE4FLFf9lKQpP2
221+
KR8NJEkK99Vh/tew6jAMll70xFrE7aF8VLXJVE7w4sQzuvHxl9Q=
222+
-----END RSA PRIVATE KEY-----
223+
`)
224+
225+
// See (https://github.com/kubernetes/kubernetes/issues/126134).
226+
func TestFallbackClient_WebSocketHTTPSProxyCausesSPDYFallback(t *testing.T) {
227+
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
228+
if err != nil {
229+
t.Errorf("https (valid hostname): proxy_test: %v", err)
230+
}
231+
232+
var proxyCalled atomic.Int64
233+
proxyHandler := utilnettesting.NewHTTPProxyHandler(t, func(req *http.Request) bool {
234+
proxyCalled.Add(1)
235+
return true
236+
})
237+
defer proxyHandler.Wait()
238+
239+
proxyServer := httptest.NewUnstartedServer(proxyHandler)
240+
proxyServer.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
241+
proxyServer.StartTLS()
242+
defer proxyServer.Close() //nolint:errcheck
243+
244+
proxyLocation, err := url.Parse(proxyServer.URL)
245+
require.NoError(t, err)
246+
247+
// Create fake SPDY server. Copy received STDIN data back onto STDOUT stream.
248+
spdyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
249+
var stdin, stdout bytes.Buffer
250+
ctx, err := createHTTPStreams(w, req, &StreamOptions{
251+
Stdin: &stdin,
252+
Stdout: &stdout,
253+
})
254+
if err != nil {
255+
w.WriteHeader(http.StatusForbidden)
256+
return
257+
}
258+
defer ctx.conn.Close() //nolint:errcheck
259+
_, err = io.Copy(ctx.stdoutStream, ctx.stdinStream)
260+
if err != nil {
261+
t.Fatalf("error copying STDIN to STDOUT: %v", err)
262+
}
263+
}))
264+
defer spdyServer.Close() //nolint:errcheck
265+
266+
backendLocation, err := url.Parse(spdyServer.URL)
267+
require.NoError(t, err)
268+
269+
clientConfig := &rest.Config{
270+
Host: spdyServer.URL,
271+
TLSClientConfig: rest.TLSClientConfig{CAData: localhostCert},
272+
Proxy: func(req *http.Request) (*url.URL, error) {
273+
return proxyLocation, nil
274+
},
275+
}
276+
277+
// Websocket with https proxy will fail in dialing (falling back to SPDY).
278+
websocketExecutor, err := NewWebSocketExecutor(clientConfig, "GET", backendLocation.String())
279+
require.NoError(t, err)
280+
spdyExecutor, err := NewSPDYExecutor(clientConfig, "POST", backendLocation)
281+
require.NoError(t, err)
282+
// Fallback to spdyExecutor with websocket https proxy error; spdyExecutor succeeds against fake spdy server.
283+
sawHTTPSProxyError := false
284+
exec, err := NewFallbackExecutor(websocketExecutor, spdyExecutor, func(err error) bool {
285+
if httpstream.IsUpgradeFailure(err) {
286+
t.Errorf("saw upgrade failure: %v", err)
287+
return true
288+
}
289+
if httpstream.IsHTTPSProxyError(err) {
290+
sawHTTPSProxyError = true
291+
t.Logf("saw https proxy error: %v", err)
292+
return true
293+
}
294+
return false
295+
})
296+
require.NoError(t, err)
297+
298+
// Generate random data, and set it up to stream on STDIN. The data will be
299+
// returned on the STDOUT buffer.
300+
randomSize := 1024 * 1024
301+
randomData := make([]byte, randomSize)
302+
if _, err := rand.Read(randomData); err != nil {
303+
t.Errorf("unexpected error reading random data: %v", err)
304+
}
305+
var stdout bytes.Buffer
306+
options := &StreamOptions{
307+
Stdin: bytes.NewReader(randomData),
308+
Stdout: &stdout,
309+
}
310+
errorChan := make(chan error)
311+
go func() {
312+
errorChan <- exec.StreamWithContext(context.Background(), *options)
313+
}()
314+
315+
select {
316+
case <-time.After(wait.ForeverTestTimeout):
317+
t.Fatalf("expect stream to be closed after connection is closed.")
318+
case err := <-errorChan:
319+
if err != nil {
320+
t.Errorf("unexpected error")
321+
}
322+
}
323+
324+
data, err := io.ReadAll(bytes.NewReader(stdout.Bytes()))
325+
if err != nil {
326+
t.Errorf("error reading the stream: %v", err)
327+
return
328+
}
329+
// Check the random data sent on STDIN was the same returned on STDOUT.
330+
if !bytes.Equal(randomData, data) {
331+
t.Errorf("unexpected data received: %d sent: %d", len(data), len(randomData))
332+
}
333+
334+
// Ensure the https proxy error was observed
335+
if !sawHTTPSProxyError {
336+
t.Errorf("expected to see https proxy error")
337+
}
338+
// Ensure the proxy was called once
339+
if e, a := int64(1), proxyCalled.Load(); e != a {
340+
t.Errorf("expected %d proxy call, got %d", e, a)
341+
}
342+
}
343+
168344
func TestFallbackClient_PrimaryAndSecondaryFail(t *testing.T) {
169345
// Create fake WebSocket server. Copy received STDIN data back onto STDOUT stream.
170346
websocketServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

Diff for: tools/remotecommand/websocket_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
v1 "k8s.io/api/core/v1"
4040
apierrors "k8s.io/apimachinery/pkg/api/errors"
4141
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
42+
"k8s.io/apimachinery/pkg/util/httpstream"
4243
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
4344
"k8s.io/apimachinery/pkg/util/remotecommand"
4445
"k8s.io/apimachinery/pkg/util/wait"
@@ -814,6 +815,42 @@ func TestWebSocketClient_BadHandshake(t *testing.T) {
814815
}
815816
}
816817

818+
// See (https://github.com/kubernetes/kubernetes/issues/126134).
819+
func TestWebSocketClient_HTTPSProxyErrorExpected(t *testing.T) {
820+
urlStr := "http://127.0.0.1/never-used" + "?" + "stdin=true" + "&" + "stdout=true"
821+
websocketLocation, err := url.Parse(urlStr)
822+
if err != nil {
823+
t.Fatalf("Unable to parse WebSocket server URL: %s", urlStr)
824+
}
825+
// proxy url with https scheme will trigger websocket dialing error.
826+
httpsProxyFunc := func(req *http.Request) (*url.URL, error) { return url.Parse("https://127.0.0.1") }
827+
exec, err := NewWebSocketExecutor(&rest.Config{Host: websocketLocation.Host, Proxy: httpsProxyFunc}, "GET", urlStr)
828+
if err != nil {
829+
t.Errorf("unexpected error creating websocket executor: %v", err)
830+
}
831+
var stdout bytes.Buffer
832+
options := &StreamOptions{
833+
Stdout: &stdout,
834+
}
835+
errorChan := make(chan error)
836+
go func() {
837+
// Start the streaming on the WebSocket "exec" client.
838+
errorChan <- exec.StreamWithContext(context.Background(), *options)
839+
}()
840+
841+
select {
842+
case <-time.After(wait.ForeverTestTimeout):
843+
t.Fatalf("expect stream to be closed after connection is closed.")
844+
case err := <-errorChan:
845+
if err == nil {
846+
t.Errorf("expected error but received none")
847+
}
848+
if !httpstream.IsHTTPSProxyError(err) {
849+
t.Errorf("expected https proxy error, got (%s)", err)
850+
}
851+
}
852+
}
853+
817854
// TestWebSocketClient_HeartbeatTimeout tests the heartbeat by forcing a
818855
// timeout by setting the ping period greater than the deadline.
819856
func TestWebSocketClient_HeartbeatTimeout(t *testing.T) {

0 commit comments

Comments
 (0)