mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
a21bf100f3
cmd/k8s-operator,k8s-operator/sessionrecording,sessionrecording,ssh/tailssh: refactor session recording functionality Refactor SSH session recording functionality (mostly the bits related to Kubernetes API server proxy 'kubectl exec' session recording): - move the session recording bits used by both Tailscale SSH and the Kubernetes API server proxy into a shared sessionrecording package, to avoid having the operator to import ssh/tailssh - move the Kubernetes API server proxy session recording functionality into a k8s-operator/sessionrecording package, add some abstractions in preparation for adding support for a second streaming protocol (WebSockets) Updates tailscale/corp#19821 Signed-off-by: Irbe Krumina <irbe@tailscale.com>
113 lines
3.3 KiB
Go
113 lines
3.3 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !plan9
|
|
|
|
package sessionrecording
|
|
|
|
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/k8s-operator/sessionrecording/fakes"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/tsnet"
|
|
"tailscale.com/tstest"
|
|
)
|
|
|
|
func Test_Hijacker(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 := &fakes.TestConn{}
|
|
ch := make(chan error)
|
|
h := &Hijacker{
|
|
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()
|
|
})
|
|
}
|
|
}
|