@ -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 ,