ssh/tailssh: restore support for recording locally

We removed it earlier in 916aa782af, but we still want to support it for some time longer.

Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
pull/8040/head
Maisem Ali 2 years ago committed by Maisem Ali
parent 4d7927047c
commit be190e990f

@ -67,6 +67,7 @@ type ipnLocalBackend interface {
WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool)
DoNoiseRequest(req *http.Request) (*http.Response, error) DoNoiseRequest(req *http.Request) (*http.Response, error)
Dialer() *tsdial.Dialer Dialer() *tsdial.Dialer
TailscaleVarRoot() string
} }
type server struct { type server struct {
@ -1154,6 +1155,11 @@ func (ss *sshSession) run() {
return return
} }
// recordSSHToLocalDisk is a deprecated dev knob to allow recording SSH sessions
// to local storage. It is only used if there is no recording configured by the
// coordination server. This will be removed in the future.
var recordSSHToLocalDisk = envknob.RegisterBool("TS_DEBUG_LOG_SSH")
// recorders returns the list of recorders to use for this session. // recorders returns the list of recorders to use for this session.
// If the final action has a non-empty list of recorders, that list is // If the final action has a non-empty list of recorders, that list is
// returned. Otherwise, the list of recorders from the initial action // returned. Otherwise, the list of recorders from the initial action
@ -1167,7 +1173,7 @@ func (ss *sshSession) recorders() ([]netip.AddrPort, *tailcfg.SSHRecorderFailure
func (ss *sshSession) shouldRecord() bool { func (ss *sshSession) shouldRecord() bool {
recs, _ := ss.recorders() recs, _ := ss.recorders()
return len(recs) > 0 return len(recs) > 0 || recordSSHToLocalDisk()
} }
type sshConnInfo struct { type sshConnInfo struct {
@ -1510,12 +1516,33 @@ func (ss *sshSession) connectToRecorder(ctx context.Context, recs []netip.AddrPo
return nil, nil, multierr.New(errs...) return nil, nil, multierr.New(errs...)
} }
func (ss *sshSession) openFileForRecording(now time.Time) (_ io.WriteCloser, err error) {
varRoot := ss.conn.srv.lb.TailscaleVarRoot()
if varRoot == "" {
return nil, errors.New("no var root for recording storage")
}
dir := filepath.Join(varRoot, "ssh-sessions")
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, err
}
f, err := os.CreateTemp(dir, fmt.Sprintf("ssh-session-%v-*.cast", now.UnixNano()))
if err != nil {
return nil, err
}
return f, nil
}
// startNewRecording starts a new SSH session recording. // startNewRecording starts a new SSH session recording.
// It may return a nil recording if recording is not available. // It may return a nil recording if recording is not available.
func (ss *sshSession) startNewRecording() (_ *recording, err error) { func (ss *sshSession) startNewRecording() (_ *recording, err error) {
recorders, onFailure := ss.recorders() recorders, onFailure := ss.recorders()
var localRecording bool
if len(recorders) == 0 { if len(recorders) == 0 {
return nil, errors.New("no recorders configured") if recordSSHToLocalDisk() {
localRecording = true
} else {
return nil, errors.New("no recorders configured")
}
} }
var w ssh.Window var w ssh.Window
@ -1539,40 +1566,45 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) {
// ss.ctx is closed when the session closes, but we don't want to break the upload at that time. // ss.ctx is closed when the session closes, but we don't want to break the upload at that time.
// Instead we want to wait for the session to close the writer when it finishes. // Instead we want to wait for the session to close the writer when it finishes.
ctx := context.Background() ctx := context.Background()
wc, errChan, err := ss.connectToRecorder(ctx, recorders) if localRecording {
if err != nil { rec.out, err = ss.openFileForRecording(now)
// TODO(catzkorn): notify control here. if err != nil {
if onFailure != nil && onFailure.RejectSessionWithMessage != "" { return nil, err
ss.logf("recording: error starting recording (rejecting session): %v", err) }
return nil, userVisibleError{ } else {
error: err, var errChan <-chan error
msg: onFailure.RejectSessionWithMessage, rec.out, errChan, err = ss.connectToRecorder(ctx, recorders)
if err != nil {
// TODO(catzkorn): notify control here.
if onFailure != nil && onFailure.RejectSessionWithMessage != "" {
ss.logf("recording: error starting recording (rejecting session): %v", err)
return nil, userVisibleError{
error: err,
msg: onFailure.RejectSessionWithMessage,
}
} }
ss.logf("recording: error starting recording (failing open): %v", err)
return nil, nil
} }
ss.logf("recording: error starting recording (failing open): %v", err) go func() {
return nil, nil err := <-errChan
if err == nil {
// Success.
return
}
// TODO(catzkorn): notify control here.
if onFailure != nil && onFailure.TerminateSessionWithMessage != "" {
ss.logf("recording: error uploading recording (closing session): %v", err)
ss.cancelCtx(userVisibleError{
error: err,
msg: onFailure.TerminateSessionWithMessage,
})
return
}
ss.logf("recording: error uploading recording (failing open): %v", err)
}()
} }
go func() {
err := <-errChan
if err == nil {
// Success.
return
}
// TODO(catzkorn): notify control here.
if onFailure != nil && onFailure.TerminateSessionWithMessage != "" {
ss.logf("recording: error uploading recording (closing session): %v", err)
ss.cancelCtx(userVisibleError{
error: err,
msg: onFailure.TerminateSessionWithMessage,
})
return
}
ss.logf("recording: error uploading recording (failing open): %v", err)
}()
rec.out = wc
ch := CastHeader{ ch := CastHeader{
Version: 2, Version: 2,
Width: w.Width, Width: w.Width,

Loading…
Cancel
Save