Browse Source

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 <[email protected]>
Maisem Ali 2 years ago
parent
commit
be190e990f
1 changed files with 64 additions and 32 deletions
  1. 64 32
      ssh/tailssh/tailssh.go

+ 64 - 32
ssh/tailssh/tailssh.go

@@ -67,6 +67,7 @@ type ipnLocalBackend interface {
 	WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool)
 	DoNoiseRequest(req *http.Request) (*http.Response, error)
 	Dialer() *tsdial.Dialer
+	TailscaleVarRoot() string
 }
 
 type server struct {
@@ -1154,6 +1155,11 @@ func (ss *sshSession) run() {
 	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.
 // If the final action has a non-empty list of recorders, that list is
 // 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 {
 	recs, _ := ss.recorders()
-	return len(recs) > 0
+	return len(recs) > 0 || recordSSHToLocalDisk()
 }
 
 type sshConnInfo struct {
@@ -1510,12 +1516,33 @@ func (ss *sshSession) connectToRecorder(ctx context.Context, recs []netip.AddrPo
 	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.
 // It may return a nil recording if recording is not available.
 func (ss *sshSession) startNewRecording() (_ *recording, err error) {
 	recorders, onFailure := ss.recorders()
+	var localRecording bool
 	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
@@ -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.
 	// Instead we want to wait for the session to close the writer when it finishes.
 	ctx := context.Background()
-	wc, 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,
+	if localRecording {
+		rec.out, err = ss.openFileForRecording(now)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		var errChan <-chan error
+		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)
-		return nil, nil
+		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)
+		}()
 	}
 
-	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{
 		Version:   2,
 		Width:     w.Width,