github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/observability/tracing/ssh/client.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 "bytes" 19 "context" 20 "fmt" 21 "net" 22 "sync" 23 24 "github.com/gravitational/trace" 25 "go.opentelemetry.io/otel/attribute" 26 semconv "go.opentelemetry.io/otel/semconv/v1.10.0" 27 oteltrace "go.opentelemetry.io/otel/trace" 28 "golang.org/x/crypto/ssh" 29 30 "github.com/gravitational/teleport/api/observability/tracing" 31 ) 32 33 // Client is a wrapper around ssh.Client that adds tracing support. 34 type Client struct { 35 *ssh.Client 36 opts []tracing.Option 37 capability tracingCapability 38 } 39 40 type tracingCapability int 41 42 const ( 43 tracingUnknown tracingCapability = iota 44 tracingUnsupported 45 tracingSupported 46 ) 47 48 // NewClient creates a new Client. 49 // 50 // The server being connected to is probed to determine if it supports 51 // ssh tracing. This is done by inspecting the version the server provides 52 // during the handshake, if it comes from a Teleport ssh server, then all 53 // payloads will be wrapped in an Envelope with tracing context. All Session 54 // and Channel created from the returned Client will honor the clients view 55 // of whether they should provide tracing context. 56 func NewClient(c ssh.Conn, chans <-chan ssh.NewChannel, reqs <-chan *ssh.Request, opts ...tracing.Option) *Client { 57 clt := &Client{ 58 Client: ssh.NewClient(c, chans, reqs), 59 opts: opts, 60 capability: tracingUnsupported, 61 } 62 63 if bytes.HasPrefix(clt.ServerVersion(), []byte("SSH-2.0-Teleport")) { 64 clt.capability = tracingSupported 65 } 66 67 return clt 68 } 69 70 // DialContext initiates a connection to the addr from the remote host. 71 // The resulting connection has a zero LocalAddr() and RemoteAddr(). 72 func (c *Client) DialContext(ctx context.Context, n, addr string) (net.Conn, error) { 73 tracer := tracing.NewConfig(c.opts).TracerProvider.Tracer(instrumentationName) 74 ctx, span := tracer.Start( 75 ctx, 76 "ssh.DialContext", 77 oteltrace.WithSpanKind(oteltrace.SpanKindClient), 78 oteltrace.WithAttributes( 79 append( 80 peerAttr(c.Conn.RemoteAddr()), 81 attribute.String("network", n), 82 attribute.String("address", addr), 83 semconv.RPCServiceKey.String("ssh.Client"), 84 semconv.RPCMethodKey.String("Dial"), 85 semconv.RPCSystemKey.String("ssh"), 86 )..., 87 ), 88 ) 89 defer span.End() 90 91 // create the wrapper while the lock is held 92 wrapper := &clientWrapper{ 93 capability: c.capability, 94 Conn: c.Client.Conn, 95 opts: c.opts, 96 ctx: ctx, 97 contexts: make(map[string][]context.Context), 98 } 99 100 conn, err := wrapper.Dial(n, addr) 101 return conn, trace.Wrap(err) 102 } 103 104 // SendRequest sends a global request, and returns the 105 // reply. If tracing is enabled, the provided payload 106 // is wrapped in an Envelope to forward any tracing context. 107 func (c *Client) SendRequest( 108 ctx context.Context, name string, wantReply bool, payload []byte, 109 ) (_ bool, _ []byte, err error) { 110 config := tracing.NewConfig(c.opts) 111 tracer := config.TracerProvider.Tracer(instrumentationName) 112 113 ctx, span := tracer.Start( 114 ctx, 115 fmt.Sprintf("ssh.GlobalRequest/%s", name), 116 oteltrace.WithSpanKind(oteltrace.SpanKindClient), 117 oteltrace.WithAttributes( 118 append( 119 peerAttr(c.Conn.RemoteAddr()), 120 attribute.Bool("want_reply", wantReply), 121 semconv.RPCServiceKey.String("ssh.Client"), 122 semconv.RPCMethodKey.String("SendRequest"), 123 semconv.RPCSystemKey.String("ssh"), 124 )..., 125 ), 126 ) 127 defer func() { tracing.EndSpan(span, err) }() 128 129 return c.Client.SendRequest( 130 name, wantReply, wrapPayload(ctx, c.capability, config.TextMapPropagator, payload), 131 ) 132 } 133 134 // OpenChannel tries to open a channel. If tracing is enabled, 135 // the provided payload is wrapped in an Envelope to forward 136 // any tracing context. 137 func (c *Client) OpenChannel( 138 ctx context.Context, name string, data []byte, 139 ) (_ *Channel, _ <-chan *ssh.Request, err error) { 140 config := tracing.NewConfig(c.opts) 141 tracer := config.TracerProvider.Tracer(instrumentationName) 142 ctx, span := tracer.Start( 143 ctx, 144 fmt.Sprintf("ssh.OpenChannel/%s", name), 145 oteltrace.WithSpanKind(oteltrace.SpanKindClient), 146 oteltrace.WithAttributes( 147 append( 148 peerAttr(c.Conn.RemoteAddr()), 149 semconv.RPCServiceKey.String("ssh.Client"), 150 semconv.RPCMethodKey.String("OpenChannel"), 151 semconv.RPCSystemKey.String("ssh"), 152 )..., 153 ), 154 ) 155 defer func() { tracing.EndSpan(span, err) }() 156 157 ch, reqs, err := c.Client.OpenChannel(name, wrapPayload(ctx, c.capability, config.TextMapPropagator, data)) 158 return &Channel{ 159 Channel: ch, 160 opts: c.opts, 161 }, reqs, err 162 } 163 164 // NewSession creates a new SSH session that is passed tracing context 165 // so that spans may be correlated properly over the ssh connection. 166 func (c *Client) NewSession(ctx context.Context) (*Session, error) { 167 tracer := tracing.NewConfig(c.opts).TracerProvider.Tracer(instrumentationName) 168 169 ctx, span := tracer.Start( 170 ctx, 171 "ssh.NewSession", 172 oteltrace.WithSpanKind(oteltrace.SpanKindClient), 173 oteltrace.WithAttributes( 174 append( 175 peerAttr(c.Conn.RemoteAddr()), 176 semconv.RPCServiceKey.String("ssh.Client"), 177 semconv.RPCMethodKey.String("NewSession"), 178 semconv.RPCSystemKey.String("ssh"), 179 )..., 180 ), 181 ) 182 defer span.End() 183 184 // create the wrapper while the lock is still held 185 wrapper := &clientWrapper{ 186 capability: c.capability, 187 Conn: c.Client.Conn, 188 opts: c.opts, 189 ctx: ctx, 190 contexts: make(map[string][]context.Context), 191 } 192 193 // get a session from the wrapper 194 session, err := wrapper.NewSession() 195 return session, trace.Wrap(err) 196 } 197 198 // clientWrapper wraps the ssh.Conn for individual ssh.Client 199 // operations to intercept internal calls by the ssh.Client to 200 // OpenChannel. This allows for internal operations within the 201 // ssh.Client to have their payload wrapped in an Envelope to 202 // forward tracing context when tracing is enabled. 203 type clientWrapper struct { 204 // Conn is the ssh.Conn that requests will be forwarded to 205 ssh.Conn 206 // capability the tracingCapability of the ssh server 207 capability tracingCapability 208 // ctx the context which should be used to create spans from 209 ctx context.Context 210 // opts the tracing options to use for creating spans with 211 opts []tracing.Option 212 213 // lock protects the context queue 214 lock sync.Mutex 215 // contexts a LIFO queue of context.Context per channel name. 216 contexts map[string][]context.Context 217 } 218 219 // NewSession opens a new Session for this client. 220 func (c *clientWrapper) NewSession() (*Session, error) { 221 // create a client that will defer to us when 222 // opening the "session" channel so that we 223 // can add an Envelope to the request 224 client := &ssh.Client{ 225 Conn: c, 226 } 227 228 session, err := client.NewSession() 229 if err != nil { 230 return nil, trace.Wrap(err) 231 } 232 233 // wrap the session so all session requests on the channel 234 // can be traced 235 return &Session{ 236 Session: session, 237 wrapper: c, 238 }, nil 239 } 240 241 // Dial initiates a connection to the addr from the remote host. 242 func (c *clientWrapper) Dial(n, addr string) (net.Conn, error) { 243 // create a client that will defer to us when 244 // opening the "direct-tcpip" channel so that we 245 // can add an Envelope to the request 246 client := &ssh.Client{ 247 Conn: c, 248 } 249 250 return client.Dial(n, addr) 251 } 252 253 // addContext adds the provided context.Context to the end of 254 // the list for the provided channel name 255 func (c *clientWrapper) addContext(ctx context.Context, name string) { 256 c.lock.Lock() 257 defer c.lock.Unlock() 258 259 c.contexts[name] = append(c.contexts[name], ctx) 260 } 261 262 // nextContext returns the first context.Context for the provided 263 // channel name 264 func (c *clientWrapper) nextContext(name string) context.Context { 265 c.lock.Lock() 266 defer c.lock.Unlock() 267 268 contexts, ok := c.contexts[name] 269 switch { 270 case !ok, len(contexts) <= 0: 271 return context.Background() 272 case len(contexts) == 1: 273 delete(c.contexts, name) 274 return contexts[0] 275 default: 276 c.contexts[name] = contexts[1:] 277 return contexts[0] 278 } 279 } 280 281 // OpenChannel tries to open a channel. If tracing is enabled, 282 // the provided payload is wrapped in an Envelope to forward 283 // any tracing context. 284 func (c *clientWrapper) OpenChannel(name string, data []byte) (_ ssh.Channel, _ <-chan *ssh.Request, err error) { 285 config := tracing.NewConfig(c.opts) 286 tracer := config.TracerProvider.Tracer(instrumentationName) 287 ctx, span := tracer.Start( 288 c.ctx, 289 fmt.Sprintf("ssh.OpenChannel/%s", name), 290 oteltrace.WithSpanKind(oteltrace.SpanKindClient), 291 oteltrace.WithAttributes( 292 append( 293 peerAttr(c.Conn.RemoteAddr()), 294 semconv.RPCServiceKey.String("ssh.Client"), 295 semconv.RPCMethodKey.String("OpenChannel"), 296 semconv.RPCSystemKey.String("ssh"), 297 )..., 298 ), 299 ) 300 defer func() { tracing.EndSpan(span, err) }() 301 302 ch, reqs, err := c.Conn.OpenChannel(name, wrapPayload(ctx, c.capability, config.TextMapPropagator, data)) 303 return channelWrapper{ 304 Channel: ch, 305 manager: c, 306 }, reqs, err 307 } 308 309 // channelWrapper wraps an ssh.Channel to allow for requests to 310 // contain tracing context. 311 type channelWrapper struct { 312 ssh.Channel 313 manager *clientWrapper 314 } 315 316 // SendRequest sends a channel request. If tracing is enabled, 317 // the provided payload is wrapped in an Envelope to forward 318 // any tracing context. 319 // 320 // It is the callers' responsibility to ensure that addContext is 321 // called with the appropriate context.Context prior to any 322 // requests being sent along the channel. 323 func (c channelWrapper) SendRequest(name string, wantReply bool, payload []byte) (_ bool, err error) { 324 config := tracing.NewConfig(c.manager.opts) 325 ctx, span := config.TracerProvider.Tracer(instrumentationName).Start( 326 c.manager.nextContext(name), 327 fmt.Sprintf("ssh.ChannelRequest/%s", name), 328 oteltrace.WithSpanKind(oteltrace.SpanKindClient), 329 oteltrace.WithAttributes( 330 attribute.Bool("want_reply", wantReply), 331 semconv.RPCServiceKey.String("ssh.Channel"), 332 semconv.RPCMethodKey.String("SendRequest"), 333 semconv.RPCSystemKey.String("ssh"), 334 ), 335 ) 336 defer func() { tracing.EndSpan(span, err) }() 337 338 return c.Channel.SendRequest(name, wantReply, wrapPayload(ctx, c.manager.capability, config.TextMapPropagator, payload)) 339 }