github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/client/proxy.go (about)

     1  /*
     2  Copyright 2022 Gravitational, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package client
    18  
    19  import (
    20  	"bufio"
    21  	"context"
    22  	"crypto/tls"
    23  	"encoding/base64"
    24  	"net"
    25  	"net/http"
    26  	"net/url"
    27  
    28  	"github.com/gravitational/trace"
    29  	"golang.org/x/net/proxy"
    30  
    31  	"github.com/gravitational/teleport/api/utils/tlsutils"
    32  )
    33  
    34  // PROXYHeaderGetter is used if present to get signed PROXY headers to propagate client's IP.
    35  // Used by proxy's web server to make calls on behalf of connected clients.
    36  type PROXYHeaderGetter func() ([]byte, error)
    37  
    38  type dialProxyConfig = dialConfig
    39  
    40  // DialProxyOption allows setting options as functional arguments to DialProxy.
    41  type DialProxyOption = DialOption
    42  
    43  // WithTLSConfig provides the dialer with the TLS config to use when using an
    44  // HTTPS proxy.
    45  func WithTLSConfig(tlsConfig *tls.Config) DialProxyOption {
    46  	return func(cfg *dialProxyConfig) {
    47  		cfg.tlsConfig = tlsConfig
    48  	}
    49  }
    50  
    51  // DialProxy creates a connection to a server via an HTTP or SOCKS5 Proxy.
    52  func DialProxy(ctx context.Context, proxyURL *url.URL, addr string, opts ...DialProxyOption) (net.Conn, error) {
    53  	var cfg dialProxyConfig
    54  	for _, opt := range opts {
    55  		opt(&cfg)
    56  	}
    57  
    58  	var dialer ContextDialer = &net.Dialer{}
    59  	if cfg.proxyHeaderGetter != nil {
    60  		dialer = NewPROXYHeaderDialer(dialer, cfg.proxyHeaderGetter)
    61  	}
    62  
    63  	return DialProxyWithDialer(ctx, proxyURL, addr, dialer, opts...)
    64  }
    65  
    66  // DialProxyWithDialer creates a connection to a server via an HTTP or SOCKS5
    67  // Proxy using a specified dialer.
    68  func DialProxyWithDialer(
    69  	ctx context.Context,
    70  	proxyURL *url.URL,
    71  	addr string,
    72  	dialer ContextDialer,
    73  	opts ...DialProxyOption,
    74  ) (net.Conn, error) {
    75  	if proxyURL == nil {
    76  		return nil, trace.BadParameter("missing proxy url")
    77  	}
    78  
    79  	var cfg dialProxyConfig
    80  	for _, opt := range opts {
    81  		opt(&cfg)
    82  	}
    83  
    84  	switch proxyURL.Scheme {
    85  	case "http", "https":
    86  		conn, err := dialProxyWithHTTPDialer(ctx, proxyURL, addr, dialer, cfg.tlsConfig)
    87  		if err != nil {
    88  			return nil, trace.Wrap(err)
    89  		}
    90  		return conn, nil
    91  	case "socks5":
    92  		conn, err := dialProxyWithSOCKSDialer(ctx, proxyURL, addr, dialer)
    93  		if err != nil {
    94  			return nil, trace.Wrap(err)
    95  		}
    96  		return conn, nil
    97  	default:
    98  		return nil, trace.BadParameter("proxy url scheme %q not supported", proxyURL.Scheme)
    99  	}
   100  }
   101  
   102  // dialProxyWithHTTPDialer creates a connection to a server via an HTTP Proxy.
   103  func dialProxyWithHTTPDialer(
   104  	ctx context.Context,
   105  	proxyURL *url.URL,
   106  	addr string,
   107  	dialer ContextDialer,
   108  	tlsConfig *tls.Config,
   109  ) (net.Conn, error) {
   110  	var conn net.Conn
   111  	var err error
   112  	if proxyURL.Scheme == "https" {
   113  		conn, err = tlsutils.TLSDial(ctx, dialer, "tcp", proxyURL.Host, tlsConfig.Clone())
   114  	} else {
   115  		conn, err = dialer.DialContext(ctx, "tcp", proxyURL.Host)
   116  	}
   117  	if err != nil {
   118  		return nil, trace.ConvertSystemError(err)
   119  	}
   120  	header := make(http.Header)
   121  	if proxyURL.User != nil {
   122  		// dont use User.String() because it performs url encoding (rfc 1738),
   123  		// which we don't want in our header
   124  		password, _ := proxyURL.User.Password()
   125  		// empty user/pass is permitted by the spec. The minimum required is a single colon.
   126  		// see: https://datatracker.ietf.org/doc/html/rfc1945#section-11
   127  		creds := proxyURL.User.Username() + ":" + password
   128  		basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(creds))
   129  		header.Add("Proxy-Authorization", basicAuth)
   130  	}
   131  	connectReq := &http.Request{
   132  		Method: http.MethodConnect,
   133  		URL:    &url.URL{Opaque: addr},
   134  		Host:   addr,
   135  		Header: header,
   136  	}
   137  
   138  	if err := connectReq.Write(conn); err != nil {
   139  		return nil, trace.Wrap(err)
   140  	}
   141  
   142  	// Read in the response. http.ReadResponse will read in the status line, mime
   143  	// headers, and potentially part of the response body. the body itself will
   144  	// not be read, but kept around so it can be read later.
   145  	br := bufio.NewReader(conn)
   146  	// Per the above comment, we're only using ReadResponse to check the status
   147  	// and then hand off the underlying connection to the caller.
   148  	// resp.Body.Close() would drain conn and close it, we don't need to do it
   149  	// here. Disabling bodyclose linter for this edge case.
   150  	//nolint:bodyclose // avoid draining the connection
   151  	resp, err := http.ReadResponse(br, connectReq)
   152  	if err != nil {
   153  		conn.Close()
   154  		return nil, trace.Wrap(err)
   155  	}
   156  	if resp.StatusCode != http.StatusOK {
   157  		conn.Close()
   158  		return nil, trace.BadParameter("unable to proxy connection: %v", resp.Status)
   159  	}
   160  
   161  	// Return a bufferedConn that wraps a net.Conn and a *bufio.Reader. this
   162  	// needs to be done because http.ReadResponse will buffer part of the
   163  	// response body in the *bufio.Reader that was passed in. reads must first
   164  	// come from anything buffered, then from the underlying connection otherwise
   165  	// data will be lost.
   166  	return &bufferedConn{
   167  		Conn:   conn,
   168  		reader: br,
   169  	}, nil
   170  }
   171  
   172  type socksDialerAdapter struct {
   173  	dialer ContextDialer
   174  }
   175  
   176  func (d *socksDialerAdapter) Dial(network, addr string) (c net.Conn, err error) {
   177  	return d.dialer.DialContext(context.Background(), network, addr)
   178  }
   179  
   180  // DialContext dials with context. Even though socks dialer interface requires just Dial() function
   181  // internally it will use dialing with context.
   182  func (d *socksDialerAdapter) DialContext(ctx context.Context, network, addr string) (c net.Conn, err error) {
   183  	return d.dialer.DialContext(ctx, network, addr)
   184  }
   185  
   186  // dialProxyWithSOCKSDialer creates a connection to a server via a SOCKS5 Proxy.
   187  func dialProxyWithSOCKSDialer(
   188  	ctx context.Context,
   189  	proxyURL *url.URL,
   190  	addr string,
   191  	dialer ContextDialer,
   192  ) (net.Conn, error) {
   193  	var proxyAuth *proxy.Auth
   194  	if proxyURL.User != nil {
   195  		proxyAuth = &proxy.Auth{
   196  			User: proxyURL.User.Username(),
   197  		}
   198  		if password, ok := proxyURL.User.Password(); ok {
   199  			proxyAuth.Password = password
   200  		}
   201  	}
   202  
   203  	socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, proxyAuth, &socksDialerAdapter{dialer: dialer})
   204  	if err != nil {
   205  		return nil, trace.Wrap(err)
   206  	}
   207  
   208  	ctxDialer, ok := socksDialer.(ContextDialer)
   209  	if !ok {
   210  		return nil, trace.Errorf("failed type assertion: wanted ContextDialer got %T", socksDialer)
   211  	}
   212  
   213  	conn, err := ctxDialer.DialContext(ctx, "tcp", addr)
   214  	if err != nil {
   215  		return nil, trace.ConvertSystemError(err)
   216  	}
   217  
   218  	return conn, nil
   219  }
   220  
   221  // bufferedConn is used when part of the data on a connection has already been
   222  // read by a *bufio.Reader. Reads will first try and read from the
   223  // *bufio.Reader and when everything has been read, reads will go to the
   224  // underlying connection.
   225  type bufferedConn struct {
   226  	net.Conn
   227  	reader *bufio.Reader
   228  }
   229  
   230  // Read first reads from the *bufio.Reader any data that has already been
   231  // buffered. Once all buffered data has been read, reads go to the net.Conn.
   232  func (bc *bufferedConn) Read(b []byte) (n int, err error) {
   233  	if bc.reader.Buffered() > 0 {
   234  		return bc.reader.Read(b)
   235  	}
   236  	return bc.Conn.Read(b)
   237  }