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  }