github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/proxy.go (about)

     1  /*
     2  
     3  Proxy support is implemented using golang's http library's built in support for proxies. This supports http connect
     4  based proxies and socks5 proxies. Proxies can be configured using: CLI flags, config.json, or environment variables.
     5  See `keybase help advanced` for information on using CLI flags. To configure a proxy using config.json run:
     6  
     7  ``` bash
     8  keybase config set proxy-type <"socks" or "http_connect">
     9  keybase config set proxy <"localhost:8080" or "username:password@localhost:8080">
    10  ```
    11  
    12  To configure a proxy using environment variables run:
    13  
    14  ``` bash
    15  export PROXY_TYPE=<"socks" or "http_connect">
    16  export PROXY=<"localhost:8080" or "username:password@localhost:8080">
    17  ```
    18  
    19  Internally, we support proxies by setting the Proxy field of http.Transport in order to use http's
    20  built in support for proxies. Note that http.Transport.Proxy does support socks5 proxies and basic auth.
    21  
    22  By default, the client reaches out to api-1.core.keybaseapi.com which has a self-signed certificate. This
    23  is actually more secure than relying on the standard CA system since we pin the client to only accept this
    24  certificate. By pinning this certificate, we make it so that any proxies that MITM TLS cannot intercept the
    25  client's traffic since even though their certificate is "trusted" according to the CA system, it isn't
    26  trusted by the client. In order to disable SSL pinning and allow TLS MITMing proxies to function, it is
    27  possible to switch the client to trust the public CA system. This can be done in one of three ways:
    28  
    29  ``` bash
    30  keybase config set disable-ssl-pinning true
    31  # OR
    32  export DISABLE_SSL_PINNING="true"
    33  # OR
    34  keybase --disable-ssl-pinning
    35  ```
    36  
    37  Note that enabling this option is NOT recommended. Enabling this option allows the proxy to view all traffic between
    38  the client and the Keybase servers.
    39  
    40  */
    41  
    42  package libkb
    43  
    44  import (
    45  	"bufio"
    46  	"context"
    47  	"crypto/tls"
    48  	"fmt"
    49  	"net"
    50  	"net/http"
    51  	"net/url"
    52  	"strings"
    53  	"sync"
    54  	"time"
    55  
    56  	"github.com/keybase/go-framed-msgpack-rpc/rpc"
    57  
    58  	"golang.org/x/net/proxy"
    59  )
    60  
    61  // Represents the different types of supported proxies
    62  type ProxyType int
    63  
    64  const (
    65  	NoProxy ProxyType = iota
    66  	Socks
    67  	HTTPConnect
    68  )
    69  
    70  // Maps a string to an enum. Used to list the different types of supported proxies and to convert
    71  // config options into the enum
    72  var ProxyTypeStrToEnum = map[string]ProxyType{"socks": Socks, "http_connect": HTTPConnect}
    73  var ProxyTypeEnumToStr = map[ProxyType]string{Socks: "socks", HTTPConnect: "http_connect", NoProxy: "no_proxy"}
    74  
    75  func GetCommaSeparatedListOfProxyTypes() string {
    76  	var proxyTypes []string
    77  	for k := range ProxyTypeStrToEnum {
    78  		proxyTypes = append(proxyTypes, k)
    79  	}
    80  	return strings.Join(proxyTypes, ",")
    81  }
    82  
    83  // Return a function that can be passed to the http library in order to configure a proxy
    84  func MakeProxy(e *Env) func(r *http.Request) (*url.URL, error) {
    85  	return func(r *http.Request) (*url.URL, error) {
    86  		proxyType := e.GetProxyType()
    87  		proxyAddress := e.GetProxy()
    88  
    89  		if proxyType == NoProxy {
    90  			// No proxy so returning nil tells it not to use a proxy
    91  			return nil, nil
    92  		}
    93  		realProxyAddress := BuildProxyAddressWithProtocol(proxyType, proxyAddress)
    94  
    95  		realProxyURL, err := url.Parse(realProxyAddress)
    96  		if err != nil {
    97  			return nil, err
    98  		}
    99  
   100  		return realProxyURL, nil
   101  	}
   102  }
   103  
   104  // Get a string that represents a proxy including the protocol needed for the proxy
   105  func BuildProxyAddressWithProtocol(proxyType ProxyType, proxyAddress string) string {
   106  	realProxyAddress := proxyAddress
   107  	if proxyType == Socks {
   108  		realProxyAddress = "socks5://" + proxyAddress
   109  	} else if proxyType == HTTPConnect && !strings.Contains(proxyAddress, "http://") && !strings.Contains(proxyAddress, "https://") {
   110  		// If they don't specify a protocol, default to http:// since it is the most common
   111  		realProxyAddress = "http://" + proxyAddress
   112  	}
   113  	return realProxyAddress
   114  }
   115  
   116  // A net.Dialer that dials via TLS
   117  type httpsDialer struct {
   118  	opts *ProxyDialOpts
   119  }
   120  
   121  func (d httpsDialer) Dial(network string, addr string) (net.Conn, error) {
   122  	// Start by making a direct dialer and dialing and then wrap TLS around it
   123  	dd := directDialer(d)
   124  	conn, err := dd.Dial(network, addr)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  	return tls.Client(conn, &tls.Config{}), err
   129  }
   130  
   131  // A net.Dialer that dials via just the standard net.Dial
   132  type directDialer struct {
   133  	opts *ProxyDialOpts
   134  }
   135  
   136  func (d directDialer) Dial(network string, addr string) (net.Conn, error) {
   137  	dialer := &net.Dialer{
   138  		Timeout:   d.opts.Timeout,
   139  		KeepAlive: d.opts.KeepAlive,
   140  	}
   141  	return dialer.Dial(network, addr)
   142  }
   143  
   144  // Get the correct upstream dialer to use for the given proxyURL
   145  func getUpstreamDialer(proxyURL *url.URL, opts *ProxyDialOpts) proxy.Dialer {
   146  	switch proxyURL.Scheme {
   147  	case "https":
   148  		return httpsDialer{opts: opts}
   149  	case "http":
   150  		fallthrough
   151  	default:
   152  		return directDialer{opts: opts}
   153  	}
   154  }
   155  
   156  // A net.Dialer that dials via a HTTP Connect proxy over the given forward dialer
   157  type httpConnectProxy struct {
   158  	proxyURL *url.URL
   159  	forward  proxy.Dialer
   160  }
   161  
   162  func newHTTPConnectProxy(proxyURL *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
   163  	s := httpConnectProxy{proxyURL: proxyURL, forward: forward}
   164  	return &s, nil
   165  }
   166  
   167  // Dial a TCP connection to the given addr (network must be TCP) via s.proxyURL
   168  func (s *httpConnectProxy) Dial(network string, addr string) (net.Conn, error) {
   169  	// We only can do TCP proxies with this function (not UDP and definitely not unix)
   170  	if network != "tcp" {
   171  		return nil, fmt.Errorf("Cannot use proxy Dial with network=%s", network)
   172  	}
   173  
   174  	// Dial a connection to the proxy using s.forward which is our upstream connection
   175  	// proxyConn is now a TCP connection to the proxy server
   176  	proxyConn, err := s.forward.Dial("tcp", s.proxyURL.Host)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	// HTTP Connect proxies work via the CONNECT verb which signals to the proxy server
   182  	// that it should treat the connection as a raw TCP stream sent to the given address
   183  	req, err := http.NewRequest("CONNECT", "//"+addr, nil)
   184  	if err != nil {
   185  		proxyConn.Close()
   186  		return nil, err
   187  	}
   188  
   189  	// We also need to set up auth for the proxy which is done via HTTP basic
   190  	// auth on the CONNECT request we are sending
   191  	if s.proxyURL.User != nil {
   192  		password, _ := s.proxyURL.User.Password()
   193  		req.SetBasicAuth(s.proxyURL.User.Username(), password)
   194  	}
   195  
   196  	// Send the HTTP request to the proxy server in order to start the TCP tunnel
   197  	err = req.Write(proxyConn)
   198  	if err != nil {
   199  		proxyConn.Close()
   200  		return nil, err
   201  	}
   202  
   203  	// Read a response and confirm that the server replied with HTTP 200 which confirms that we started the
   204  	// TCP tunnel. Note that we don't expect any additional body to the request since this is now just an open
   205  	// TCP tunnel
   206  	resp, err := http.ReadResponse(bufio.NewReader(proxyConn), req)
   207  	if err != nil {
   208  		proxyConn.Close()
   209  		return nil, err
   210  	}
   211  	defer resp.Body.Close()
   212  
   213  	if resp.StatusCode != 200 {
   214  		proxyConn.Close()
   215  		err = fmt.Errorf("Failed to connect to proxy server, status code: %d", resp.StatusCode)
   216  		return nil, err
   217  	}
   218  
   219  	// proxyConn is now a TCP connection to the proxy server which forwards to addr. It is the responsibility
   220  	// of the caller to Close() proxyConn
   221  	return proxyConn, nil
   222  }
   223  
   224  var registerLock = sync.Mutex{}
   225  var hasBeenRegistered = false
   226  
   227  // Must be called in order for the proxy library to support HTTP connect proxies. The proxy library uses a map to store
   228  // this information which can lead to a `fatal error: concurrent map writes` so we use a lock to serialize it and a
   229  // bool to make it so we only register once (avoid acquiring a lock every time we start a proxy connection).
   230  func registerHTTPConnectProxies() {
   231  	if !hasBeenRegistered {
   232  		registerLock.Lock()
   233  		proxy.RegisterDialerType("http", newHTTPConnectProxy)
   234  		proxy.RegisterDialerType("https", newHTTPConnectProxy)
   235  		hasBeenRegistered = true
   236  		registerLock.Unlock()
   237  	}
   238  }
   239  
   240  type ProxyDialOpts struct {
   241  	Timeout   time.Duration
   242  	KeepAlive time.Duration
   243  }
   244  
   245  // The equivalent of net.Dial except it uses the proxy configured in Env
   246  func ProxyDial(env *Env, network string, address string) (net.Conn, error) {
   247  	// Set the timeout to an exceedingly large number so it never times out
   248  	return ProxyDialTimeout(env, network, address, 100*365*24*time.Hour)
   249  }
   250  
   251  // The equivalent of net.DialTimeout except it uses the proxy configured in Env
   252  func ProxyDialTimeout(env *Env, network string, address string, timeout time.Duration) (net.Conn, error) {
   253  	return ProxyDialWithOpts(context.TODO(), env, network, address, &ProxyDialOpts{Timeout: timeout})
   254  }
   255  
   256  func ProxyDialWithOpts(ctx context.Context, env *Env, network string, address string, opts *ProxyDialOpts) (net.Conn, error) {
   257  	if env.GetProxyType() == NoProxy {
   258  		dialer := &net.Dialer{
   259  			Timeout:   opts.Timeout,
   260  			KeepAlive: opts.KeepAlive,
   261  		}
   262  		return dialer.DialContext(ctx, network, address)
   263  	}
   264  	registerHTTPConnectProxies()
   265  	proxyURLStr := BuildProxyAddressWithProtocol(env.GetProxyType(), env.GetProxy())
   266  	proxyURL, err := url.Parse(proxyURLStr)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	dialer, err := proxy.FromURL(proxyURL, getUpstreamDialer(proxyURL, opts))
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	// Currently proxy.Dialer does not support DialContext. This is being actively worked on and will probably
   276  	// land in the next go release, but for now we are emulating it with a goroutine and channels
   277  	// See: https://github.com/golang/go/issues/17759
   278  	doneCh := make(chan net.Conn, 1)
   279  	errCh := make(chan error, 1)
   280  	go func() {
   281  		conn, err := dialer.Dial(network, address)
   282  		if err != nil {
   283  			errCh <- err
   284  		} else {
   285  			doneCh <- conn
   286  		}
   287  	}()
   288  	select {
   289  	case <-ctx.Done():
   290  		return nil, ctx.Err()
   291  	case conn := <-doneCh:
   292  		return conn, nil
   293  	case err := <-errCh:
   294  		return nil, err
   295  	}
   296  }
   297  
   298  func ProxyHTTPClient(g *GlobalContext, env *Env, instrumentationTag string) *http.Client {
   299  	xprt := NewInstrumentedRoundTripper(g, func(*http.Request) string { return instrumentationTag },
   300  		&http.Transport{
   301  			Proxy: MakeProxy(env),
   302  		})
   303  	client := &http.Client{
   304  		Transport: xprt,
   305  	}
   306  	return client
   307  }
   308  
   309  // The equivalent of http.Get except it uses the proxy configured in Env
   310  // `instrumentationTag` should be a static tag for all requests identifying the
   311  // type of request we are proxying so we don't leak URL information to the
   312  // instrumenter.
   313  func ProxyHTTPGet(g *GlobalContext, env *Env, u, instrumentationTag string) (*http.Response, error) {
   314  	client := ProxyHTTPClient(g, env, instrumentationTag)
   315  	return client.Get(u)
   316  }
   317  
   318  // A struct that implements rpc.Dialable from go-framed-msgpack-rpc
   319  type ProxyDialable struct {
   320  	env       *Env
   321  	Timeout   time.Duration
   322  	KeepAlive time.Duration
   323  }
   324  
   325  func NewProxyDialable(env *Env) *ProxyDialable {
   326  	return &ProxyDialable{env: env}
   327  }
   328  
   329  func (pd *ProxyDialable) SetOpts(timeout time.Duration, keepAlive time.Duration) {
   330  	pd.Timeout = timeout
   331  	pd.KeepAlive = keepAlive
   332  }
   333  
   334  func (pd *ProxyDialable) Dial(ctx context.Context, network string, addr string) (net.Conn, error) {
   335  	return ProxyDialTimeout(pd.env, network, addr, pd.Timeout)
   336  }
   337  
   338  // Test that ProxyDialable implements rpc.Dialable
   339  var _ rpc.Dialable = (*ProxyDialable)(nil)