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 }