mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 21:15:39 +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>
112 lines
3.2 KiB
Go
112 lines
3.2 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !plan9
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"tailscale.com/client/tailscale/apitype"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/tsnet"
|
|
"tailscale.com/tstest"
|
|
)
|
|
|
|
func Test_SPDYHijacker(t *testing.T) {
|
|
zl, err := zap.NewDevelopment()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
failOpen bool
|
|
failRecorderConnect bool // fail initial connect to the recorder
|
|
failRecorderConnPostConnect bool // send error down the error channel
|
|
wantsConnClosed bool
|
|
wantsSetupErr bool
|
|
}{
|
|
{
|
|
name: "setup succeeds, conn stays open",
|
|
},
|
|
{
|
|
name: "setup fails, policy is to fail open, conn stays open",
|
|
failOpen: true,
|
|
failRecorderConnect: true,
|
|
},
|
|
{
|
|
name: "setup fails, policy is to fail closed, conn is closed",
|
|
failRecorderConnect: true,
|
|
wantsSetupErr: true,
|
|
wantsConnClosed: true,
|
|
},
|
|
{
|
|
name: "connection fails post-initial connect, policy is to fail open, conn stays open",
|
|
failRecorderConnPostConnect: true,
|
|
failOpen: true,
|
|
},
|
|
{
|
|
name: "connection fails post-initial connect, policy is to fail closed, conn is closed",
|
|
failRecorderConnPostConnect: true,
|
|
wantsConnClosed: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tc := &testConn{}
|
|
ch := make(chan error)
|
|
h := &spdyHijacker{
|
|
connectToRecorder: func(context.Context, []netip.AddrPort, func(context.Context, string, string) (net.Conn, error)) (wc io.WriteCloser, rec []*tailcfg.SSHRecordingAttempt, _ <-chan error, err error) {
|
|
if tt.failRecorderConnect {
|
|
err = errors.New("test")
|
|
}
|
|
return wc, rec, ch, err
|
|
},
|
|
failOpen: tt.failOpen,
|
|
who: &apitype.WhoIsResponse{Node: &tailcfg.Node{}, UserProfile: &tailcfg.UserProfile{}},
|
|
log: zl.Sugar(),
|
|
ts: &tsnet.Server{},
|
|
req: &http.Request{URL: &url.URL{}},
|
|
}
|
|
ctx := context.Background()
|
|
_, err := h.setUpRecording(ctx, tc)
|
|
if (err != nil) != tt.wantsSetupErr {
|
|
t.Errorf("spdyHijacker.setupRecording() error = %v, wantErr %v", err, tt.wantsSetupErr)
|
|
return
|
|
}
|
|
if tt.failRecorderConnPostConnect {
|
|
select {
|
|
case ch <- errors.New("err"):
|
|
case <-time.After(time.Second * 15):
|
|
t.Errorf("error from recorder conn was not read within 15 seconds")
|
|
}
|
|
}
|
|
timeout := time.Second * 20
|
|
// TODO (irbekrm): cover case where an error is received
|
|
// over channel and the failure policy is to fail open
|
|
// (test that connection remains open over some period
|
|
// of time).
|
|
if err := tstest.WaitFor(timeout, func() (err error) {
|
|
if tt.wantsConnClosed != tc.isClosed() {
|
|
return fmt.Errorf("got connection state: %t, wants connection state: %t", tc.isClosed(), tt.wantsConnClosed)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
t.Errorf("connection did not reach the desired state within %s", timeout.String())
|
|
}
|
|
ctx.Done()
|
|
})
|
|
}
|
|
}
|