mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-27 03:55:36 +00:00
ba517ab388
cmd/k8s-operator,ssh/tailssh,tsnet: optionally record kubectl exec sessions The Kubernetes operator's API server proxy, when it receives a request for 'kubectl exec' session now reads 'RecorderAddrs', 'EnforceRecorder' fields from tailcfg.KubernetesCapRule. If 'RecorderAddrs' is set to one or more addresses (of a tsrecorder instance(s)), it attempts to connect to those and sends the session contents to the recorder before forwarding the request to the kube API server. If connection cannot be established or fails midway, it is only allowed if 'EnforceRecorder' is not true (fail open). Updates tailscale/corp#19821 Signed-off-by: Irbe Krumina <irbe@tailscale.com> Co-authored-by: Maisem Ali <maisem@tailscale.com>
294 lines
7.9 KiB
Go
294 lines
7.9 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !plan9
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"encoding/binary"
|
|
"io"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func Test_spdyFrame_Parse(t *testing.T) {
|
|
zl, err := zap.NewDevelopment()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
gotBytes []byte
|
|
wantFrame spdyFrame
|
|
wantOk bool
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "control_frame_syn_stream",
|
|
gotBytes: []byte{0x80, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0},
|
|
wantFrame: spdyFrame{
|
|
Version: 3,
|
|
Type: SYN_STREAM,
|
|
Ctrl: true,
|
|
Raw: []byte{0x80, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0},
|
|
Payload: []byte{},
|
|
},
|
|
wantOk: true,
|
|
},
|
|
{
|
|
name: "control_frame_syn_reply",
|
|
gotBytes: []byte{0x80, 0x3, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0},
|
|
wantFrame: spdyFrame{
|
|
Ctrl: true,
|
|
Version: 3,
|
|
Type: SYN_REPLY,
|
|
Raw: []byte{0x80, 0x3, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0},
|
|
Payload: []byte{},
|
|
},
|
|
wantOk: true,
|
|
},
|
|
{
|
|
name: "control_frame_headers",
|
|
gotBytes: []byte{0x80, 0x3, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0},
|
|
wantFrame: spdyFrame{
|
|
Ctrl: true,
|
|
Version: 3,
|
|
Type: 8,
|
|
Raw: []byte{0x80, 0x3, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0},
|
|
Payload: []byte{},
|
|
},
|
|
wantOk: true,
|
|
},
|
|
{
|
|
name: "data_frame_stream_id_5",
|
|
gotBytes: []byte{0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x0},
|
|
wantFrame: spdyFrame{
|
|
Payload: []byte{},
|
|
StreamID: 5,
|
|
Raw: []byte{0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x0},
|
|
},
|
|
wantOk: true,
|
|
},
|
|
{
|
|
name: "frame_with_incomplete_header",
|
|
gotBytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
|
},
|
|
{
|
|
name: "frame_with_incomplete_payload",
|
|
gotBytes: []byte{0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x2}, // header specifies payload length of 2
|
|
},
|
|
{
|
|
name: "control_bit_set_not_spdy_frame",
|
|
gotBytes: []byte{0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, // header specifies payload length of 2
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "control_bit_not_set_not_spdy_frame",
|
|
gotBytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, // header specifies payload length of 2
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sf := &spdyFrame{}
|
|
gotOk, err := sf.Parse(tt.gotBytes, zl.Sugar())
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("spdyFrame.Parse() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if gotOk != tt.wantOk {
|
|
t.Errorf("spdyFrame.Parse() = %v, want %v", gotOk, tt.wantOk)
|
|
}
|
|
if diff := cmp.Diff(*sf, tt.wantFrame); diff != "" {
|
|
t.Errorf("Unexpected SPDY frame (-got +want):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_spdyFrame_parseHeaders(t *testing.T) {
|
|
zl, err := zap.NewDevelopment()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
isCtrl bool
|
|
payload []byte
|
|
typ ControlFrameType
|
|
wantHeader http.Header
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "syn_stream_with_header",
|
|
payload: payload(t, map[string]string{"Streamtype": "stdin"}, SYN_STREAM, 1),
|
|
typ: SYN_STREAM,
|
|
isCtrl: true,
|
|
wantHeader: header(map[string]string{"Streamtype": "stdin"}),
|
|
},
|
|
{
|
|
name: "syn_ping",
|
|
payload: payload(t, nil, SYN_PING, 0),
|
|
typ: SYN_PING,
|
|
isCtrl: true,
|
|
},
|
|
{
|
|
name: "syn_reply_headers",
|
|
payload: payload(t, map[string]string{"foo": "bar", "bar": "baz"}, SYN_REPLY, 0),
|
|
typ: SYN_REPLY,
|
|
isCtrl: true,
|
|
wantHeader: header(map[string]string{"foo": "bar", "bar": "baz"}),
|
|
},
|
|
{
|
|
name: "syn_reply_no_headers",
|
|
payload: payload(t, nil, SYN_REPLY, 0),
|
|
typ: SYN_REPLY,
|
|
isCtrl: true,
|
|
},
|
|
{
|
|
name: "syn_stream_too_short_payload",
|
|
payload: []byte{0, 1, 2, 3, 4},
|
|
typ: SYN_STREAM,
|
|
isCtrl: true,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "syn_reply_too_short_payload",
|
|
payload: []byte{0, 1, 2},
|
|
typ: SYN_REPLY,
|
|
isCtrl: true,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "syn_ping_too_short_payload",
|
|
payload: []byte{0, 1, 2},
|
|
typ: SYN_PING,
|
|
isCtrl: true,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "not_a_control_frame",
|
|
payload: []byte{0, 1, 2, 3},
|
|
typ: SYN_PING,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
var reader zlibReader
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sf := &spdyFrame{
|
|
Ctrl: tt.isCtrl,
|
|
Type: tt.typ,
|
|
Payload: tt.payload,
|
|
}
|
|
gotHeader, err := sf.parseHeaders(&reader, zl.Sugar())
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("spdyFrame.parseHeaders() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if !reflect.DeepEqual(gotHeader, tt.wantHeader) {
|
|
t.Errorf("spdyFrame.parseHeaders() = %v, want %v", gotHeader, tt.wantHeader)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// payload takes a control frame type and a map with 0 or more header keys and
|
|
// values and returns a SPDY control frame payload with the header as SPDY zlib
|
|
// compressed header name/value block. The payload is padded with arbitrary
|
|
// bytes to ensure the header name/value block is in the correct position for
|
|
// the frame type.
|
|
func payload(t *testing.T, headerM map[string]string, typ ControlFrameType, streamID int) []byte {
|
|
t.Helper()
|
|
|
|
buf := bytes.NewBuffer([]byte{})
|
|
writeControlFramePayloadBeforeHeaders(t, buf, typ, streamID)
|
|
if len(headerM) == 0 {
|
|
return buf.Bytes()
|
|
}
|
|
|
|
w, err := zlib.NewWriterLevelDict(buf, zlib.BestCompression, spdyTxtDictionary)
|
|
if err != nil {
|
|
t.Fatalf("error creating new zlib writer: %v", err)
|
|
}
|
|
if len(headerM) != 0 {
|
|
writeHeaderValueBlock(t, w, headerM)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("error writing headers: %v", err)
|
|
}
|
|
w.Flush()
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// writeControlFramePayloadBeforeHeaders writes to w N bytes, N being the number
|
|
// of bytes that control frame payload for that control frame is required to
|
|
// contain before the name/value header block.
|
|
func writeControlFramePayloadBeforeHeaders(t *testing.T, w io.Writer, typ ControlFrameType, streamID int) {
|
|
t.Helper()
|
|
switch typ {
|
|
case SYN_STREAM:
|
|
// needs 10 bytes in payload before any headers
|
|
if err := binary.Write(w, binary.BigEndian, uint32(streamID)); err != nil {
|
|
t.Fatalf("writing streamID: %v", err)
|
|
}
|
|
if err := binary.Write(w, binary.BigEndian, [6]byte{0}); err != nil {
|
|
t.Fatalf("writing payload: %v", err)
|
|
}
|
|
case SYN_REPLY:
|
|
// needs 4 bytes in payload before any headers
|
|
if err := binary.Write(w, binary.BigEndian, uint32(0)); err != nil {
|
|
t.Fatalf("writing payload: %v", err)
|
|
}
|
|
case SYN_PING:
|
|
// needs 4 bytes in payload
|
|
if err := binary.Write(w, binary.BigEndian, uint32(0)); err != nil {
|
|
t.Fatalf("writing payload: %v", err)
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected frame type: %v", typ)
|
|
}
|
|
}
|
|
|
|
// writeHeaderValue block takes http.Header and zlib writer, writes the headers
|
|
// as SPDY zlib compressed bytes to the writer.
|
|
// Adopted from https://github.com/moby/spdystream/blob/v0.2.0/spdy/write.go#L171-L198 (which is also what Kubernetes uses).
|
|
func writeHeaderValueBlock(t *testing.T, w io.Writer, headerM map[string]string) {
|
|
t.Helper()
|
|
h := header(headerM)
|
|
if err := binary.Write(w, binary.BigEndian, uint32(len(h))); err != nil {
|
|
t.Fatalf("error writing header block length: %v", err)
|
|
}
|
|
for name, values := range h {
|
|
if err := binary.Write(w, binary.BigEndian, uint32(len(name))); err != nil {
|
|
t.Fatalf("error writing name length for name %q: %v", name, err)
|
|
}
|
|
name = strings.ToLower(name)
|
|
if _, err := io.WriteString(w, name); err != nil {
|
|
t.Fatalf("error writing name %q: %v", name, err)
|
|
}
|
|
v := strings.Join(values, string(headerSep))
|
|
if err := binary.Write(w, binary.BigEndian, uint32(len(v))); err != nil {
|
|
t.Fatalf("error writing value length for value %q: %v", v, err)
|
|
}
|
|
if _, err := io.WriteString(w, v); err != nil {
|
|
t.Fatalf("error writing value %q: %v", v, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func header(hs map[string]string) http.Header {
|
|
h := make(http.Header, len(hs))
|
|
for key, val := range hs {
|
|
h.Add(key, val)
|
|
}
|
|
return h
|
|
}
|