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)