mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 22:15:51 +00:00
a15ff1bade
cmd/k8s-operator,k8s-operator/sessionrecording: support recording WebSocket sessions Kubernetes currently supports two streaming protocols, SPDY and WebSockets. WebSockets are replacing SPDY, see https://github.com/kubernetes/enhancements/issues/4006. We were currently only supporting SPDY, erroring out if session was not SPDY and relying on the kube's built-in SPDY fallback. This PR: - adds support for parsing contents of 'kubectl exec' sessions streamed over WebSockets - adds logic to distinguish 'kubectl exec' requests for a SPDY/WebSockets sessions and call the relevant handler Updates tailscale/corp#19821 Signed-off-by: Irbe Krumina <irbe@tailscale.com> Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com>
216 lines
7.0 KiB
Go
216 lines
7.0 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !plan9
|
|
|
|
package ws
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"math/rand"
|
|
|
|
"go.uber.org/zap"
|
|
"golang.org/x/net/websocket"
|
|
)
|
|
|
|
func Test_msg_Parse(t *testing.T) {
|
|
zl, err := zap.NewDevelopment()
|
|
if err != nil {
|
|
t.Fatalf("error creating a test logger: %v", err)
|
|
}
|
|
testMask := [4]byte{1, 2, 3, 4}
|
|
bs126, bs126Len := bytesSlice2ByteLen(t)
|
|
bs127, bs127Len := byteSlice8ByteLen(t)
|
|
tests := []struct {
|
|
name string
|
|
b []byte
|
|
initialPayload []byte
|
|
wantPayload []byte
|
|
wantIsFinalized bool
|
|
wantStreamID uint32
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "single_fragment_stdout_stream_no_payload_no_mask",
|
|
b: []byte{0x82, 0x1, 0x1},
|
|
wantPayload: nil,
|
|
wantIsFinalized: true,
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "single_fragment_stderr_steam_no_payload_has_mask",
|
|
b: append([]byte{0x82, 0x81, 0x1, 0x2, 0x3, 0x4}, maskedBytes(testMask, []byte{0x2})...),
|
|
wantPayload: nil,
|
|
wantIsFinalized: true,
|
|
wantStreamID: 2,
|
|
},
|
|
{
|
|
name: "single_fragment_stdout_stream_no_mask_has_payload",
|
|
b: []byte{0x82, 0x3, 0x1, 0x7, 0x8},
|
|
wantPayload: []byte{0x7, 0x8},
|
|
wantIsFinalized: true,
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "single_fragment_stdout_stream_has_mask_has_payload",
|
|
b: append([]byte{0x82, 0x83, 0x1, 0x2, 0x3, 0x4}, maskedBytes(testMask, []byte{0x1, 0x7, 0x8})...),
|
|
wantPayload: []byte{0x7, 0x8},
|
|
wantIsFinalized: true,
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "initial_fragment_stdout_stream_no_mask_has_payload",
|
|
b: []byte{0x2, 0x3, 0x1, 0x7, 0x8},
|
|
wantPayload: []byte{0x7, 0x8},
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "initial_fragment_stdout_stream_has_mask_has_payload",
|
|
b: append([]byte{0x2, 0x83, 0x1, 0x2, 0x3, 0x4}, maskedBytes(testMask, []byte{0x1, 0x7, 0x8})...),
|
|
wantPayload: []byte{0x7, 0x8},
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "subsequent_fragment_stdout_stream_no_mask_has_payload",
|
|
b: []byte{0x0, 0x3, 0x1, 0x7, 0x8},
|
|
initialPayload: []byte{0x1, 0x2, 0x3},
|
|
wantPayload: []byte{0x1, 0x2, 0x3, 0x7, 0x8},
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "subsequent_fragment_stdout_stream_has_mask_has_payload",
|
|
b: append([]byte{0x0, 0x83, 0x1, 0x2, 0x3, 0x4}, maskedBytes(testMask, []byte{0x1, 0x7, 0x8})...),
|
|
initialPayload: []byte{0x1, 0x2, 0x3},
|
|
wantPayload: []byte{0x1, 0x2, 0x3, 0x7, 0x8},
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "final_fragment_stdout_stream_no_mask_has_payload",
|
|
b: []byte{0x80, 0x3, 0x1, 0x7, 0x8},
|
|
initialPayload: []byte{0x1, 0x2, 0x3},
|
|
wantIsFinalized: true,
|
|
wantPayload: []byte{0x1, 0x2, 0x3, 0x7, 0x8},
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "final_fragment_stdout_stream_has_mask_has_payload",
|
|
b: append([]byte{0x80, 0x83, 0x1, 0x2, 0x3, 0x4}, maskedBytes(testMask, []byte{0x1, 0x7, 0x8})...),
|
|
initialPayload: []byte{0x1, 0x2, 0x3},
|
|
wantIsFinalized: true,
|
|
wantPayload: []byte{0x1, 0x2, 0x3, 0x7, 0x8},
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "single_large_fragment_no_mask_length_hint_126",
|
|
b: append(append([]byte{0x80, 0x7e}, bs126Len...), append([]byte{0x1}, bs126...)...),
|
|
wantIsFinalized: true,
|
|
wantPayload: bs126,
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "single_large_fragment_no_mask_length_hint_127",
|
|
b: append(append([]byte{0x80, 0x7f}, bs127Len...), append([]byte{0x1}, bs127...)...),
|
|
wantIsFinalized: true,
|
|
wantPayload: bs127,
|
|
wantStreamID: 1,
|
|
},
|
|
{
|
|
name: "zero_length_bytes",
|
|
b: []byte{},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
msg := &message{
|
|
typ: binaryMessage,
|
|
payload: tt.initialPayload,
|
|
}
|
|
if _, err := msg.Parse(tt.b, zl.Sugar()); (err != nil) != tt.wantErr {
|
|
t.Errorf("msg.Parse() = %v, wantsErr: %t", err, tt.wantErr)
|
|
}
|
|
if msg.isFinalized != tt.wantIsFinalized {
|
|
t.Errorf("wants message to be finalized: %t, got: %t", tt.wantIsFinalized, msg.isFinalized)
|
|
}
|
|
if msg.streamID.Load() != tt.wantStreamID {
|
|
t.Errorf("wants stream ID: %d, got: %d", tt.wantStreamID, msg.streamID.Load())
|
|
}
|
|
if !reflect.DeepEqual(msg.payload, tt.wantPayload) {
|
|
t.Errorf("unexpected message payload after Parse, wants %b got %b", tt.wantPayload, msg.payload)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test_msg_Parse_Rand calls Parse with a randomly generated input to verify
|
|
// that it doesn't panic.
|
|
func Test_msg_Parse_Rand(t *testing.T) {
|
|
zl, err := zap.NewDevelopment()
|
|
if err != nil {
|
|
t.Fatalf("error creating a test logger: %v", err)
|
|
}
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
for i := range 100 {
|
|
n := r.Intn(4096)
|
|
b := make([]byte, n)
|
|
_, err := r.Read(b)
|
|
if err != nil {
|
|
t.Fatalf("error generating random byte slice: %v", err)
|
|
}
|
|
msg := message{typ: binaryMessage}
|
|
f := func() {
|
|
msg.Parse(b, zl.Sugar())
|
|
}
|
|
testPanic(t, f, fmt.Sprintf("[%d] Parse panicked running with byte slice of length %d: %v", i, n, r))
|
|
}
|
|
}
|
|
|
|
// byteSlice2ByteLen generates a number that represents websocket message fragment length and is stored in an 8 byte slice.
|
|
// Returns the byte slice with the length as well as a slice of arbitrary bytes of the given length.
|
|
// This is used to generate test input representing websocket message with payload length hint 126.
|
|
// https://www.rfc-editor.org/rfc/rfc6455#section-5.2
|
|
func bytesSlice2ByteLen(t *testing.T) ([]byte, []byte) {
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
var n uint16
|
|
n = uint16(rand.Intn(65535 - 1)) // space for and additional 1 byte stream ID
|
|
b := make([]byte, n)
|
|
_, err := r.Read(b)
|
|
if err != nil {
|
|
t.Fatalf("error generating random byte slice: %v ", err)
|
|
}
|
|
bb := make([]byte, 2)
|
|
binary.BigEndian.PutUint16(bb, n+1) // + stream ID
|
|
return b, bb
|
|
}
|
|
|
|
// byteSlice8ByteLen generates a number that represents websocket message fragment length and is stored in an 8 byte slice.
|
|
// Returns the byte slice with the length as well as a slice of arbitrary bytes of the given length.
|
|
// This is used to generate test input representing websocket message with payload length hint 127.
|
|
// https://www.rfc-editor.org/rfc/rfc6455#section-5.2
|
|
func byteSlice8ByteLen(t *testing.T) ([]byte, []byte) {
|
|
nanos := time.Now().UnixNano()
|
|
t.Logf("Creating random source with seed %v", nanos)
|
|
r := rand.New(rand.NewSource(nanos))
|
|
var n uint64
|
|
n = uint64(rand.Intn(websocket.DefaultMaxPayloadBytes - 1)) // space for and additional 1 byte stream ID
|
|
t.Logf("byteSlice8ByteLen: generating message payload of length %d", n)
|
|
b := make([]byte, n)
|
|
_, err := r.Read(b)
|
|
if err != nil {
|
|
t.Fatalf("error generating random byte slice: %v ", err)
|
|
}
|
|
bb := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(bb, n+1) // + stream ID
|
|
return b, bb
|
|
}
|
|
|
|
func maskedBytes(mask [4]byte, b []byte) []byte {
|
|
maskBytes(mask, b)
|
|
return b
|
|
}
|