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 }