mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-22 08:51:41 +00:00
ssh/tailssh: restore support for recording locally
We removed it earlier in 916aa782af5d43ccfa92f6245201796df212fb8a, but we still want to support it for some time longer. Updates tailscale/corp#9967 Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
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…
x
Reference in New Issue
Block a user