github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/observability/tracing/ssh/session.go (about)

     1  // Copyright 2022 Gravitational, Inc
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ssh
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"github.com/gravitational/trace"
    24  	"go.opentelemetry.io/otel/attribute"
    25  	semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
    26  	oteltrace "go.opentelemetry.io/otel/trace"
    27  	"golang.org/x/crypto/ssh"
    28  
    29  	"github.com/gravitational/teleport/api/constants"
    30  	"github.com/gravitational/teleport/api/observability/tracing"
    31  )
    32  
    33  // Session is a wrapper around ssh.Session that adds tracing support
    34  type Session struct {
    35  	*ssh.Session
    36  	wrapper *clientWrapper
    37  }
    38  
    39  // SendRequest sends an out-of-band channel request on the SSH channel
    40  // underlying the session.
    41  func (s *Session) SendRequest(ctx context.Context, name string, wantReply bool, payload []byte) (bool, error) {
    42  	config := tracing.NewConfig(s.wrapper.opts)
    43  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
    44  		ctx,
    45  		fmt.Sprintf("ssh.SessionRequest/%s", name),
    46  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
    47  		oteltrace.WithAttributes(
    48  			attribute.Bool("want_reply", wantReply),
    49  			semconv.RPCServiceKey.String("ssh.Session"),
    50  			semconv.RPCMethodKey.String("SendRequest"),
    51  			semconv.RPCSystemKey.String("ssh"),
    52  		),
    53  	)
    54  	defer span.End()
    55  
    56  	// no need to wrap payload here, the session's channel wrapper will do it for us
    57  	s.wrapper.addContext(ctx, name)
    58  	ok, err := s.Session.SendRequest(name, wantReply, payload)
    59  	return ok, trace.Wrap(err)
    60  }
    61  
    62  // Setenv sets an environment variable that will be applied to any
    63  // command executed by Shell or Run.
    64  func (s *Session) Setenv(ctx context.Context, name, value string) error {
    65  	const request = "env"
    66  	config := tracing.NewConfig(s.wrapper.opts)
    67  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
    68  		ctx,
    69  		fmt.Sprintf("ssh.Setenv/%s", name),
    70  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
    71  		oteltrace.WithAttributes(
    72  			semconv.RPCServiceKey.String("ssh.Session"),
    73  			semconv.RPCMethodKey.String("SendRequest"),
    74  			semconv.RPCSystemKey.String("ssh"),
    75  		),
    76  	)
    77  	defer span.End()
    78  
    79  	s.wrapper.addContext(ctx, request)
    80  	return trace.Wrap(s.Session.Setenv(name, value))
    81  }
    82  
    83  // SetEnvs sets environment variables that will be applied to any
    84  // command executed by Shell or Run. If the server does not handle
    85  // [EnvsRequest] requests then the client falls back to sending individual
    86  // "env" requests until all provided environment variables have been set
    87  // or an error was received.
    88  func (s *Session) SetEnvs(ctx context.Context, envs map[string]string) error {
    89  	config := tracing.NewConfig(s.wrapper.opts)
    90  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
    91  		ctx,
    92  		"ssh.SetEnvs",
    93  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
    94  		oteltrace.WithAttributes(
    95  			semconv.RPCServiceKey.String("ssh.Session"),
    96  			semconv.RPCMethodKey.String("SendRequest"),
    97  			semconv.RPCSystemKey.String("ssh"),
    98  		),
    99  	)
   100  	defer span.End()
   101  
   102  	if len(envs) == 0 {
   103  		return nil
   104  	}
   105  
   106  	// If the server isn't Teleport fallback to individual "env" requests
   107  	if !strings.HasPrefix(string(s.wrapper.ServerVersion()), "SSH-2.0-Teleport") {
   108  		return trace.Wrap(s.setEnvFallback(ctx, envs))
   109  	}
   110  
   111  	raw, err := json.Marshal(envs)
   112  	if err != nil {
   113  		return trace.Wrap(err)
   114  	}
   115  
   116  	s.wrapper.addContext(ctx, EnvsRequest)
   117  	ok, err := s.Session.SendRequest(EnvsRequest, true, ssh.Marshal(EnvsReq{EnvsJSON: raw}))
   118  	if err != nil {
   119  		return trace.Wrap(err)
   120  	}
   121  
   122  	// The server does not handle EnvsRequest requests so fall back
   123  	// to sending individual requests.
   124  	if !ok {
   125  		return trace.Wrap(s.setEnvFallback(ctx, envs))
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  // setEnvFallback sends an "env" request for each item in envs.
   132  func (s *Session) setEnvFallback(ctx context.Context, envs map[string]string) error {
   133  	for k, v := range envs {
   134  		if err := s.Setenv(ctx, k, v); err != nil {
   135  			return trace.Wrap(err, "failed to set environment variable %s", k)
   136  		}
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  // RequestPty requests the association of a pty with the session on the remote host.
   143  func (s *Session) RequestPty(ctx context.Context, term string, h, w int, termmodes ssh.TerminalModes) error {
   144  	const request = "pty-req"
   145  	config := tracing.NewConfig(s.wrapper.opts)
   146  	tracer := config.TracerProvider.Tracer(instrumentationName)
   147  	ctx, span := tracer.Start(
   148  		ctx,
   149  		fmt.Sprintf("ssh.RequestPty/%s", term),
   150  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
   151  		oteltrace.WithAttributes(
   152  			semconv.RPCServiceKey.String("ssh.Session"),
   153  			semconv.RPCMethodKey.String("SendRequest"),
   154  			semconv.RPCSystemKey.String("ssh"),
   155  			attribute.Int("width", w),
   156  			attribute.Int("height", h),
   157  		),
   158  	)
   159  	defer span.End()
   160  
   161  	s.wrapper.addContext(ctx, request)
   162  	return trace.Wrap(s.Session.RequestPty(term, h, w, termmodes))
   163  }
   164  
   165  // RequestSubsystem requests the association of a subsystem with the session on the remote host.
   166  // A subsystem is a predefined command that runs in the background when the ssh session is initiated.
   167  func (s *Session) RequestSubsystem(ctx context.Context, subsystem string) error {
   168  	const request = "subsystem"
   169  	config := tracing.NewConfig(s.wrapper.opts)
   170  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
   171  		ctx,
   172  		fmt.Sprintf("ssh.RequestSubsystem/%s", subsystem),
   173  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
   174  		oteltrace.WithAttributes(
   175  			semconv.RPCServiceKey.String("ssh.Session"),
   176  			semconv.RPCMethodKey.String("SendRequest"),
   177  			semconv.RPCSystemKey.String("ssh"),
   178  		),
   179  	)
   180  	defer span.End()
   181  
   182  	s.wrapper.addContext(ctx, request)
   183  	return trace.Wrap(s.Session.RequestSubsystem(subsystem))
   184  }
   185  
   186  // WindowChange informs the remote host about a terminal window dimension change to h rows and w columns.
   187  func (s *Session) WindowChange(ctx context.Context, h, w int) error {
   188  	const request = "window-change"
   189  	config := tracing.NewConfig(s.wrapper.opts)
   190  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
   191  		ctx,
   192  		"ssh.WindowChange",
   193  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
   194  		oteltrace.WithAttributes(
   195  			semconv.RPCServiceKey.String("ssh.Session"),
   196  			semconv.RPCMethodKey.String("SendRequest"),
   197  			semconv.RPCSystemKey.String("ssh"),
   198  			attribute.Int("height", h),
   199  			attribute.Int("width", w),
   200  		),
   201  	)
   202  	defer span.End()
   203  
   204  	s.wrapper.addContext(ctx, request)
   205  	return trace.Wrap(s.Session.WindowChange(h, w))
   206  }
   207  
   208  // Signal sends the given signal to the remote process.
   209  // sig is one of the SIG* constants.
   210  func (s *Session) Signal(ctx context.Context, sig ssh.Signal) error {
   211  	const request = "signal"
   212  	config := tracing.NewConfig(s.wrapper.opts)
   213  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
   214  		ctx,
   215  		fmt.Sprintf("ssh.Signal/%s", sig),
   216  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
   217  		oteltrace.WithAttributes(
   218  			semconv.RPCServiceKey.String("ssh.Session"),
   219  			semconv.RPCMethodKey.String("SendRequest"),
   220  			semconv.RPCSystemKey.String("ssh"),
   221  		),
   222  	)
   223  	defer span.End()
   224  
   225  	s.wrapper.addContext(ctx, request)
   226  	return trace.Wrap(s.Session.Signal(sig))
   227  }
   228  
   229  // Start runs cmd on the remote host. Typically, the remote
   230  // server passes cmd to the shell for interpretation.
   231  // A Session only accepts one call to Run, Start or Shell.
   232  func (s *Session) Start(ctx context.Context, cmd string) error {
   233  	const request = "exec"
   234  	config := tracing.NewConfig(s.wrapper.opts)
   235  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
   236  		ctx,
   237  		fmt.Sprintf("ssh.Start/%s", cmd),
   238  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
   239  		oteltrace.WithAttributes(
   240  			semconv.RPCServiceKey.String("ssh.Session"),
   241  			semconv.RPCMethodKey.String("SendRequest"),
   242  			semconv.RPCSystemKey.String("ssh"),
   243  		),
   244  	)
   245  	defer span.End()
   246  
   247  	s.wrapper.addContext(ctx, request)
   248  	return trace.Wrap(s.Session.Start(cmd))
   249  }
   250  
   251  // Shell starts a login shell on the remote host. A Session only
   252  // accepts one call to Run, Start, Shell, Output, or CombinedOutput.
   253  func (s *Session) Shell(ctx context.Context) error {
   254  	const request = "shell"
   255  	config := tracing.NewConfig(s.wrapper.opts)
   256  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
   257  		ctx,
   258  		"ssh.Shell",
   259  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
   260  		oteltrace.WithAttributes(
   261  			semconv.RPCServiceKey.String("ssh.Session"),
   262  			semconv.RPCMethodKey.String("SendRequest"),
   263  			semconv.RPCSystemKey.String("ssh"),
   264  		),
   265  	)
   266  	defer span.End()
   267  
   268  	s.wrapper.addContext(ctx, request)
   269  	return trace.Wrap(s.Session.Shell())
   270  }
   271  
   272  // Run runs cmd on the remote host. Typically, the remote
   273  // server passes cmd to the shell for interpretation.
   274  // A Session only accepts one call to Run, Start, Shell, Output,
   275  // or CombinedOutput.
   276  //
   277  // The returned error is nil if the command runs, has no problems
   278  // copying stdin, stdout, and stderr, and exits with a zero exit
   279  // status.
   280  //
   281  // If the remote server does not send an exit status, an error of type
   282  // *ExitMissingError is returned. If the command completes
   283  // unsuccessfully or is interrupted by a signal, the error is of type
   284  // *ExitError. Other error types may be returned for I/O problems.
   285  func (s *Session) Run(ctx context.Context, cmd string) error {
   286  	const request = "exec"
   287  	config := tracing.NewConfig(s.wrapper.opts)
   288  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
   289  		ctx,
   290  		fmt.Sprintf("ssh.Run/%s", cmd),
   291  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
   292  		oteltrace.WithAttributes(
   293  			semconv.RPCServiceKey.String("ssh.Session"),
   294  			semconv.RPCMethodKey.String("SendRequest"),
   295  			semconv.RPCSystemKey.String("ssh"),
   296  		),
   297  	)
   298  	defer span.End()
   299  
   300  	s.wrapper.addContext(ctx, request)
   301  	return trace.Wrap(s.Session.Run(cmd))
   302  }
   303  
   304  // Output runs cmd on the remote host and returns its standard output.
   305  func (s *Session) Output(ctx context.Context, cmd string) ([]byte, error) {
   306  	const request = "exec"
   307  	config := tracing.NewConfig(s.wrapper.opts)
   308  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
   309  		ctx,
   310  		fmt.Sprintf("ssh.Output/%s", cmd),
   311  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
   312  		oteltrace.WithAttributes(
   313  			semconv.RPCServiceKey.String("ssh.Session"),
   314  			semconv.RPCMethodKey.String("SendRequest"),
   315  			semconv.RPCSystemKey.String("ssh"),
   316  		),
   317  	)
   318  	defer span.End()
   319  
   320  	s.wrapper.addContext(ctx, request)
   321  	output, err := s.Session.Output(cmd)
   322  	return output, trace.Wrap(err)
   323  }
   324  
   325  // CombinedOutput runs cmd on the remote host and returns its combined
   326  // standard output and standard error.
   327  func (s *Session) CombinedOutput(ctx context.Context, cmd string) ([]byte, error) {
   328  	const request = "exec"
   329  	config := tracing.NewConfig(s.wrapper.opts)
   330  	ctx, span := config.TracerProvider.Tracer(instrumentationName).Start(
   331  		ctx,
   332  		fmt.Sprintf("ssh.CombinedOutput/%s", cmd),
   333  		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
   334  		oteltrace.WithAttributes(
   335  			semconv.RPCServiceKey.String("ssh.Session"),
   336  			semconv.RPCMethodKey.String("SendRequest"),
   337  			semconv.RPCSystemKey.String("ssh"),
   338  		),
   339  	)
   340  	defer span.End()
   341  
   342  	s.wrapper.addContext(ctx, request)
   343  	output, err := s.Session.CombinedOutput(cmd)
   344  	return output, trace.Wrap(err)
   345  }
   346  
   347  // sendFileTransferDecision will send a "file-transfer-decision@goteleport.com" ssh request
   348  func (s *Session) sendFileTransferDecision(ctx context.Context, requestID string, approved bool) error {
   349  	req := &FileTransferDecisionReq{
   350  		RequestID: requestID,
   351  		Approved:  approved,
   352  	}
   353  	_, err := s.SendRequest(ctx, constants.FileTransferDecision, true, ssh.Marshal(req))
   354  	return trace.Wrap(err)
   355  }
   356  
   357  // ApproveFileTransferRequest sends a "file-transfer-decision@goteleport.com" ssh request
   358  // The ssh request will have the request ID and Approved: true
   359  func (s *Session) ApproveFileTransferRequest(ctx context.Context, requestID string) error {
   360  	return trace.Wrap(s.sendFileTransferDecision(ctx, requestID, true))
   361  }
   362  
   363  // DenyFileTransferRequest sends a "file-transfer-decision@goteleport.com" ssh request
   364  // The ssh request will have the request ID and Approved: false
   365  func (s *Session) DenyFileTransferRequest(ctx context.Context, requestID string) error {
   366  	return trace.Wrap(s.sendFileTransferDecision(ctx, requestID, false))
   367  }
   368  
   369  // RequestFileTransfer sends a "file-transfer-request@goteleport.com" ssh request that will create a new file transfer request
   370  // and notify the parties in an ssh session
   371  func (s *Session) RequestFileTransfer(ctx context.Context, req FileTransferReq) error {
   372  	_, err := s.SendRequest(ctx, constants.InitiateFileTransfer, true, ssh.Marshal(req))
   373  	return trace.Wrap(err)
   374  }