diff --git a/api/client/client.go b/api/client/client.go index 4f90044d15aae..1567f3aaecfcb 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -285,7 +285,7 @@ type connectParams struct { } func authConnect(ctx context.Context, params connectParams) (*Client, error) { - dialer := NewDirectDialer(params.cfg.KeepAlivePeriod, params.cfg.DialTimeout) + dialer := NewDialer(params.cfg.KeepAlivePeriod, params.cfg.DialTimeout) clt := newClient(params.cfg, dialer, params.tlsConfig) if err := clt.dialGRPC(ctx, params.addr); err != nil { return nil, trace.Wrap(err, "failed to connect to addr %v as an auth server", params.addr) diff --git a/api/client/contextdialer.go b/api/client/contextdialer.go index 2c2d08e29e894..b07af84097e9a 100644 --- a/api/client/contextdialer.go +++ b/api/client/contextdialer.go @@ -44,14 +44,26 @@ func (f ContextDialerFunc) DialContext(ctx context.Context, network, addr string return f(ctx, network, addr) } -// NewDirectDialer makes a new dialer to connect directly to an Auth server. -func NewDirectDialer(keepAlivePeriod, dialTimeout time.Duration) ContextDialer { +// newDirectDialer makes a new dialer to connect directly to an Auth server. +func newDirectDialer(keepAlivePeriod, dialTimeout time.Duration) ContextDialer { return &net.Dialer{ Timeout: dialTimeout, KeepAlive: keepAlivePeriod, } } +// NewDialer makes a new dialer that connects to an Auth server either directly or via an HTTP proxy, depending +// on the environment. +func NewDialer(keepAlivePeriod, dialTimeout time.Duration) ContextDialer { + return ContextDialerFunc(func(ctx context.Context, network, addr string) (net.Conn, error) { + dialer := newDirectDialer(keepAlivePeriod, dialTimeout) + if proxyAddr := GetProxyAddress(addr); proxyAddr != "" { + return DialProxyWithDialer(ctx, proxyAddr, addr, dialer) + } + return dialer.DialContext(ctx, network, addr) + }) +} + // NewProxyDialer makes a dialer to connect to an Auth server through the SSH reverse tunnel on the proxy. // The dialer will ping the web client to discover the tunnel proxy address on each dial. func NewProxyDialer(ssh ssh.ClientConfig, keepAlivePeriod, dialTimeout time.Duration, discoveryAddr string, insecure bool) ContextDialer { @@ -72,7 +84,7 @@ func NewProxyDialer(ssh ssh.ClientConfig, keepAlivePeriod, dialTimeout time.Dura // newTunnelDialer makes a dialer to connect to an Auth server through the SSH reverse tunnel on the proxy. func newTunnelDialer(ssh ssh.ClientConfig, keepAlivePeriod, dialTimeout time.Duration) ContextDialer { - dialer := NewDirectDialer(keepAlivePeriod, dialTimeout) + dialer := newDirectDialer(keepAlivePeriod, dialTimeout) return ContextDialerFunc(func(ctx context.Context, network, addr string) (conn net.Conn, err error) { conn, err = dialer.DialContext(ctx, network, addr) if err != nil { diff --git a/lib/utils/proxy/noproxy.go b/api/client/noproxy.go similarity index 91% rename from lib/utils/proxy/noproxy.go rename to api/client/noproxy.go index d9dcdb5f3d8a9..a6115d20c6a5a 100644 --- a/lib/utils/proxy/noproxy.go +++ b/api/client/noproxy.go @@ -7,13 +7,13 @@ // This is the low-level Transport implementation of RoundTripper. // The high-level interface is in client.go. -package proxy +package client import ( "os" "strings" - "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" ) // useProxy reports whether requests to addr should use a proxy, @@ -24,7 +24,7 @@ func useProxy(addr string) bool { return true } var noProxy string - for _, env := range []string{teleport.NoProxy, strings.ToLower(teleport.NoProxy)} { + for _, env := range []string{constants.NoProxy, strings.ToLower(constants.NoProxy)} { noProxy = os.Getenv(env) if noProxy != "" { break diff --git a/api/client/proxy.go b/api/client/proxy.go new file mode 100644 index 0000000000000..9d33a1ab23730 --- /dev/null +++ b/api/client/proxy.go @@ -0,0 +1,150 @@ +/* +Copyright 2022 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "bufio" + "context" + "net" + "net/http" + "net/url" + "os" + "strings" + + "github.com/gravitational/teleport/api/constants" + "github.com/gravitational/trace" + log "github.com/sirupsen/logrus" +) + +// DialProxy creates a connection to a server via an HTTP Proxy. +func DialProxy(ctx context.Context, proxyAddr, addr string) (net.Conn, error) { + return DialProxyWithDialer(ctx, proxyAddr, addr, &net.Dialer{}) +} + +// DialProxyWithDialer creates a connection to a server via an HTTP Proxy using a specified dialer. +func DialProxyWithDialer(ctx context.Context, proxyAddr, addr string, dialer ContextDialer) (net.Conn, error) { + conn, err := dialer.DialContext(ctx, "tcp", proxyAddr) + if err != nil { + log.Warnf("Unable to dial to proxy: %v: %v.", proxyAddr, err) + return nil, trace.ConvertSystemError(err) + } + + connectReq := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: make(http.Header), + } + + if err := connectReq.Write(conn); err != nil { + log.Warnf("Unable to write to proxy: %v.", err) + return nil, trace.Wrap(err) + } + + // Read in the response. http.ReadResponse will read in the status line, mime + // headers, and potentially part of the response body. the body itself will + // not be read, but kept around so it can be read later. + br := bufio.NewReader(conn) + // Per the above comment, we're only using ReadResponse to check the status + // and then hand off the underlying connection to the caller. + // resp.Body.Close() would drain conn and close it, we don't need to do it + // here. Disabling bodyclose linter for this edge case. + //nolint:bodyclose + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + conn.Close() + log.Warnf("Unable to read response: %v.", err) + return nil, trace.Wrap(err) + } + if resp.StatusCode != http.StatusOK { + conn.Close() + return nil, trace.BadParameter("unable to proxy connection: %v", resp.Status) + } + + // Return a bufferedConn that wraps a net.Conn and a *bufio.Reader. this + // needs to be done because http.ReadResponse will buffer part of the + // response body in the *bufio.Reader that was passed in. reads must first + // come from anything buffered, then from the underlying connection otherwise + // data will be lost. + return &bufferedConn{ + Conn: conn, + reader: br, + }, nil +} + +// GetProxyAddress gets the HTTP proxy address to use for a given address, if any. +func GetProxyAddress(addr string) string { + envs := []string{ + constants.HTTPSProxy, + strings.ToLower(constants.HTTPSProxy), + constants.HTTPProxy, + strings.ToLower(constants.HTTPProxy), + } + + for _, v := range envs { + envAddr := os.Getenv(v) + if envAddr == "" { + continue + } + proxyAddr, err := parse(envAddr) + if err != nil { + log.Debugf("Unable to parse environment variable %q: %q.", v, envAddr) + continue + } + log.Debugf("Successfully parsed environment variable %q: %q to %q.", v, envAddr, proxyAddr) + if !useProxy(addr) { + log.Debugf("Matched NO_PROXY override for %q: %q, going to ignore proxy variable.", v, envAddr) + return "" + } + return proxyAddr + } + + log.Debugf("No valid environment variables found.") + return "" +} + +// bufferedConn is used when part of the data on a connection has already been +// read by a *bufio.Reader. Reads will first try and read from the +// *bufio.Reader and when everything has been read, reads will go to the +// underlying connection. +type bufferedConn struct { + net.Conn + reader *bufio.Reader +} + +// Read first reads from the *bufio.Reader any data that has already been +// buffered. Once all buffered data has been read, reads go to the net.Conn. +func (bc *bufferedConn) Read(b []byte) (n int, err error) { + if bc.reader.Buffered() > 0 { + return bc.reader.Read(b) + } + return bc.Conn.Read(b) +} + +// parse will extract the host:port of the proxy to dial to. If the +// value is not prefixed by "http", then it will prepend "http" and try. +func parse(addr string) (string, error) { + proxyurl, err := url.Parse(addr) + if err != nil || !strings.HasPrefix(proxyurl.Scheme, "http") { + proxyurl, err = url.Parse("http://" + addr) + if err != nil { + return "", trace.Wrap(err) + } + } + + return proxyurl.Host, nil +} diff --git a/lib/utils/proxy/proxy_test.go b/api/client/proxy_test.go similarity index 93% rename from lib/utils/proxy/proxy_test.go rename to api/client/proxy_test.go index a5c986acd89c5..29cffc12e1b4f 100644 --- a/lib/utils/proxy/proxy_test.go +++ b/api/client/proxy_test.go @@ -14,22 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package proxy +package client import ( "fmt" - "os" "testing" - "github.com/gravitational/teleport/lib/utils" "github.com/stretchr/testify/require" ) -func TestMain(m *testing.M) { - utils.InitLoggerForTests() - os.Exit(m.Run()) -} - func TestGetProxyAddress(t *testing.T) { type env struct { name string @@ -96,7 +89,7 @@ func TestGetProxyAddress(t *testing.T) { for _, env := range tt.env { t.Setenv(env.name, env.val) } - p := getProxyAddress(tt.targetAddr) + p := GetProxyAddress(tt.targetAddr) require.Equal(t, tt.proxyAddr, p) }) } diff --git a/api/client/webclient/webclient.go b/api/client/webclient/webclient.go index b82e16f7ac9fe..626c0cffaf029 100644 --- a/api/client/webclient/webclient.go +++ b/api/client/webclient/webclient.go @@ -33,6 +33,7 @@ import ( "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/utils" + "golang.org/x/net/http/httpproxy" "github.com/gravitational/trace" log "github.com/sirupsen/logrus" @@ -46,6 +47,9 @@ func newWebClient(insecure bool, pool *x509.CertPool) *http.Client { RootCAs: pool, InsecureSkipVerify: insecure, }, + Proxy: func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + }, }, } } diff --git a/api/client/webclient/webclient_test.go b/api/client/webclient/webclient_test.go index 556d6cab96478..5e8caf6da0c56 100644 --- a/api/client/webclient/webclient_test.go +++ b/api/client/webclient/webclient_test.go @@ -289,3 +289,28 @@ func TestExtract(t *testing.T) { }) } } + +func TestNewWebClientRespectHTTPProxy(t *testing.T) { + t.Setenv("HTTPS_PROXY", "fakeproxy.example.com:9999") + client := newWebClient(false /* insecure */, nil /* pool */) + // resp should be nil, so there will be no body to close. + //nolint:bodyclose + resp, err := client.Get("https://fakedomain.example.com") + // Client should try to proxy through nonexistent server at localhost. + require.Error(t, err, "GET unexpectedly succeeded: %+v", resp) + require.Contains(t, err.Error(), "proxyconnect") + require.Contains(t, err.Error(), "lookup fakeproxy.example.com") + require.Contains(t, err.Error(), "no such host") +} + +func TestNewWebClientNoProxy(t *testing.T) { + t.Setenv("HTTPS_PROXY", "fakeproxy.example.com:9999") + t.Setenv("NO_PROXY", "fakedomain.example.com") + client := newWebClient(false /* insecure */, nil /* pool */) + //nolint:bodyclose + resp, err := client.Get("https://fakedomain.example.com") + require.Error(t, err, "GET unexpectedly succeeded: %+v", resp) + require.NotContains(t, err.Error(), "proxyconnect") + require.Contains(t, err.Error(), "lookup fakedomain.example.com") + require.Contains(t, err.Error(), "no such host") +} diff --git a/api/constants/constants.go b/api/constants/constants.go index 4d8a2edc33dcf..07ed678ff28fb 100644 --- a/api/constants/constants.go +++ b/api/constants/constants.go @@ -180,3 +180,15 @@ const ( // KubeTeleportProxyALPNPrefix is a SNI Kubernetes prefix used for distinguishing the Kubernetes HTTP traffic. KubeTeleportProxyALPNPrefix = "kube-teleport-proxy-alpn." ) + +const ( + // HTTPSProxy is an environment variable pointing to a HTTPS proxy. + HTTPSProxy = "HTTPS_PROXY" + + // HTTPProxy is an environment variable pointing to a HTTP proxy. + HTTPProxy = "HTTP_PROXY" + + // NoProxy is an environment variable matching the cases + // when HTTPS_PROXY or HTTP_PROXY is ignored + NoProxy = "NO_PROXY" +) diff --git a/constants.go b/constants.go index bed3002bac77a..f763fbac31186 100644 --- a/constants.go +++ b/constants.go @@ -61,18 +61,6 @@ const ( HTTPNextProtoTLS = "http/1.1" ) -const ( - // HTTPSProxy is an environment variable pointing to a HTTPS proxy. - HTTPSProxy = "HTTPS_PROXY" - - // HTTPProxy is an environment variable pointing to a HTTP proxy. - HTTPProxy = "HTTP_PROXY" - - // NoProxy is an environment variable matching the cases - // when HTTPS_PROXY or HTTP_PROXY is ignored - NoProxy = "NO_PROXY" -) - const ( // TOTPValidityPeriod is the number of seconds a TOTP token is valid. TOTPValidityPeriod uint = 30 diff --git a/lib/auth/clt.go b/lib/auth/clt.go index 2a168fb18618c..998682a72518f 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -128,7 +128,7 @@ func NewHTTPClient(cfg client.Config, tls *tls.Config, params ...roundtrip.Clien if len(cfg.Addrs) == 0 { return nil, trace.BadParameter("no addresses to dial") } - contextDialer := client.NewDirectDialer(cfg.KeepAlivePeriod, cfg.DialTimeout) + contextDialer := client.NewDialer(cfg.KeepAlivePeriod, cfg.DialTimeout) dialer = client.ContextDialerFunc(func(ctx context.Context, network, _ string) (conn net.Conn, err error) { for _, addr := range cfg.Addrs { conn, err = contextDialer.DialContext(ctx, network, addr) diff --git a/lib/client/api.go b/lib/client/api.go index 252258b08b04d..9aa5699e1bab7 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -68,6 +68,7 @@ import ( "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/agentconn" + "github.com/gravitational/teleport/lib/utils/proxy" "github.com/gravitational/trace" @@ -2184,37 +2185,26 @@ func (tc *TeleportClient) connectToProxy(ctx context.Context) (*ProxyClient, err }, nil } -func makeProxySSHClientWithTLSWrapper(tc *TeleportClient, sshConfig *ssh.ClientConfig) (*ssh.Client, error) { - cfg := tc.Config - clientTLSConf, err := tc.loadTLSConfig() - if err != nil { - return nil, trace.Wrap(err) - } - - clientTLSConf.NextProtos = []string{string(alpncommon.ProtocolProxySSH)} - clientTLSConf.InsecureSkipVerify = cfg.InsecureSkipVerify - - tlsConn, err := tls.Dial("tcp", cfg.WebProxyAddr, clientTLSConf) - if err != nil { - return nil, trace.Wrap(err, "failed to dial tls %v", cfg.WebProxyAddr) - } - c, chans, reqs, err := ssh.NewClientConn(tlsConn, cfg.WebProxyAddr, sshConfig) - if err != nil { - // tlsConn is closed inside ssh.NewClientConn function - return nil, trace.Wrap(err, "failed to authenticate with proxy %v", cfg.WebProxyAddr) - } - return ssh.NewClient(c, chans, reqs), nil -} - func makeProxySSHClient(tc *TeleportClient, sshConfig *ssh.ClientConfig) (*ssh.Client, error) { if tc.Config.TLSRoutingEnabled { return makeProxySSHClientWithTLSWrapper(tc, sshConfig) } - client, err := ssh.Dial("tcp", tc.Config.SSHProxyAddr, sshConfig) + return makeProxySSHClientDirect(tc, sshConfig) +} + +func makeProxySSHClientDirect(tc *TeleportClient, sshConfig *ssh.ClientConfig) (*ssh.Client, error) { + dialer := proxy.DialerFromEnvironment(tc.Config.SSHProxyAddr) + return dialer.Dial("tcp", tc.Config.SSHProxyAddr, sshConfig) +} + +func makeProxySSHClientWithTLSWrapper(tc *TeleportClient, sshConfig *ssh.ClientConfig) (*ssh.Client, error) { + tlsConfig, err := tc.loadTLSConfig() if err != nil { - return nil, trace.Wrap(err, "failed to authenticate with proxy %v", tc.Config.SSHProxyAddr) + return nil, trace.Wrap(err) } - return client, nil + tlsConfig.NextProtos = []string{string(alpncommon.ProtocolProxySSH)} + dialer := proxy.DialerFromEnvironment(tc.Config.WebProxyAddr, proxy.WithALPNDialer(tlsConfig)) + return dialer.Dial("tcp", tc.Config.WebProxyAddr, sshConfig) } func (tc *TeleportClient) rootClusterName() (string, error) { diff --git a/lib/client/https_client.go b/lib/client/https_client.go index 4d1c94807dcec..c9f01aead2cb0 100644 --- a/lib/client/https_client.go +++ b/lib/client/https_client.go @@ -27,6 +27,7 @@ import ( apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/lib/httplib" "github.com/gravitational/teleport/lib/utils" + "golang.org/x/net/http/httpproxy" "github.com/gravitational/roundtrip" "github.com/gravitational/trace" @@ -41,6 +42,9 @@ func NewInsecureWebClient() *http.Client { return &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, + Proxy: func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + }, }, } } @@ -54,6 +58,9 @@ func newClientWithPool(pool *x509.CertPool) *http.Client { return &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, + Proxy: func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + }, }, } } diff --git a/lib/client/https_client_test.go b/lib/client/https_client_test.go new file mode 100644 index 0000000000000..66adb430ef035 --- /dev/null +++ b/lib/client/https_client_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2022 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewInsecureWebClientHTTPProxy(t *testing.T) { + t.Setenv("HTTPS_PROXY", "fakeproxy.example.com:9999") + client := NewInsecureWebClient() + // resp should be nil, so there will be no body to close. + //nolint:bodyclose + resp, err := client.Get("https://fakedomain.example.com") + // Client should try to proxy through nonexistent server at localhost. + require.Error(t, err, "GET unexpectedly succeeded: %+v", resp) + require.Contains(t, err.Error(), "proxyconnect") + require.Contains(t, err.Error(), "lookup fakeproxy.example.com") + require.Contains(t, err.Error(), "no such host") +} + +func TestNewInsecureWebClientNoProxy(t *testing.T) { + t.Setenv("HTTPS_PROXY", "fakeproxy.example.com:9999") + t.Setenv("NO_PROXY", "fakedomain.example.com") + client := NewInsecureWebClient() + //nolint:bodyclose + resp, err := client.Get("https://fakedomain.example.com") + require.Error(t, err, "GET unexpectedly succeeded: %+v", resp) + require.NotContains(t, err.Error(), "proxyconnect") + require.Contains(t, err.Error(), "lookup fakedomain.example.com") + require.Contains(t, err.Error(), "no such host") +} + +func TestNewClientWithPoolHTTPProxy(t *testing.T) { + t.Setenv("HTTPS_PROXY", "fakeproxy.example.com:9999") + client := newClientWithPool(nil) + // resp should be nil, so there will be no body to close. + //nolint:bodyclose + resp, err := client.Get("https://fakedomain.example.com") + // Client should try to proxy through nonexistent server at localhost. + require.Error(t, err, "GET unexpectedly succeeded: %+v", resp) + require.Contains(t, err.Error(), "proxyconnect") + require.Contains(t, err.Error(), "lookup fakeproxy.example.com") + require.Contains(t, err.Error(), "no such host") +} + +func TestNewClientWithPoolNoProxy(t *testing.T) { + t.Setenv("HTTPS_PROXY", "fakeproxy.example.com:9999") + t.Setenv("NO_PROXY", "fakedomain.example.com") + client := newClientWithPool(nil) + //nolint:bodyclose + resp, err := client.Get("https://fakedomain.example.com") + require.Error(t, err, "GET unexpectedly succeeded: %+v", resp) + require.NotContains(t, err.Error(), "proxyconnect") + require.Contains(t, err.Error(), "lookup fakedomain.example.com") + require.Contains(t, err.Error(), "no such host") +} diff --git a/lib/reversetunnel/agent.go b/lib/reversetunnel/agent.go index e367b0841dbe2..ce74f963d4167 100644 --- a/lib/reversetunnel/agent.go +++ b/lib/reversetunnel/agent.go @@ -22,6 +22,7 @@ package reversetunnel import ( "context" + "crypto/tls" "fmt" "sync" "time" @@ -35,6 +36,7 @@ import ( "github.com/gravitational/teleport/lib" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/reversetunnel/track" + alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/proxy" @@ -283,7 +285,9 @@ func (a *Agent) connect() (conn *ssh.Client, err error) { } if a.reverseTunnelDetails != nil && a.reverseTunnelDetails.TLSRoutingEnabled { - opts = append(opts, proxy.WithALPNDialer()) + opts = append(opts, proxy.WithALPNDialer(&tls.Config{ + NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)}, + })) } for _, authMethod := range a.authMethods { diff --git a/lib/reversetunnel/transport.go b/lib/reversetunnel/transport.go index 80667e96b7fda..6e7abb5d9b560 100644 --- a/lib/reversetunnel/transport.go +++ b/lib/reversetunnel/transport.go @@ -18,6 +18,7 @@ package reversetunnel import ( "context" + "crypto/tls" "encoding/json" "fmt" "io" @@ -32,6 +33,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/lib/auth" + alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/proxy" @@ -95,7 +97,9 @@ func (t *TunnelAuthDialer) DialContext(ctx context.Context, _, _ string) (net.Co // address thus the ping call will always fail. t.Log.Debugf("Failed to ping web proxy %q addr: %v", addr.Addr, err) } else if resp.Proxy.TLSRoutingEnabled { - opts = append(opts, proxy.WithALPNDialer()) + opts = append(opts, proxy.WithALPNDialer(&tls.Config{ + NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)}, + })) } dialer := proxy.DialerFromEnvironment(addr.Addr, opts...) diff --git a/lib/utils/proxy/proxy.go b/lib/utils/proxy/proxy.go index 9c934cc0277fa..90c8226cced13 100644 --- a/lib/utils/proxy/proxy.go +++ b/lib/utils/proxy/proxy.go @@ -16,21 +16,16 @@ limitations under the License. package proxy import ( - "bufio" "context" "crypto/tls" "net" - "net/http" - "net/url" - "os" - "strings" "time" "github.com/gravitational/trace" "github.com/gravitational/teleport" + apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/utils/sshutils" - alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common" "github.com/gravitational/teleport/lib/utils" "golang.org/x/crypto/ssh" @@ -56,7 +51,7 @@ func dialWithDeadline(network string, addr string, config *ssh.ClientConfig) (*s // dialALPNWithDeadline allows connecting to Teleport in single-port mode. SSH protocol is wrapped into // TLS connection where TLS ALPN protocol is set to ProtocolReverseTunnel allowing ALPN Proxy to route the // incoming connection to ReverseTunnel proxy service. -func dialALPNWithDeadline(network string, addr string, config *ssh.ClientConfig, insecure bool) (*ssh.Client, error) { +func (d directDial) dialALPNWithDeadline(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { dialer := &net.Dialer{ Timeout: config.Timeout, } @@ -64,11 +59,11 @@ func dialALPNWithDeadline(network string, addr string, config *ssh.ClientConfig, if err != nil { return nil, trace.Wrap(err) } - tlsConn, err := tls.DialWithDialer(dialer, network, addr, &tls.Config{ - NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)}, - InsecureSkipVerify: insecure, - ServerName: address.Host(), - }) + conf, err := d.getTLSConfig(address) + if err != nil { + return nil, trace.Wrap(err) + } + tlsConn, err := tls.DialWithDialer(dialer, network, addr, conf) if err != nil { return nil, trace.Wrap(err) } @@ -85,16 +80,29 @@ type Dialer interface { } type directDial struct { - // tlsRoutingEnabled indicates that proxy is running in TLSRouting mode. - tlsRoutingEnabled bool // insecure is whether to skip certificate validation. insecure bool + // tlsRoutingEnabled indicates that proxy is running in TLSRouting mode. + tlsRoutingEnabled bool + // tlsConfig is the TLS config to use. + tlsConfig *tls.Config +} + +// getTLSConfig configures the dialers TLS config for a specified address. +func (d directDial) getTLSConfig(addr *utils.NetAddr) (*tls.Config, error) { + if d.tlsConfig == nil { + return nil, trace.BadParameter("TLS config was nil") + } + tlsConfig := d.tlsConfig.Clone() + tlsConfig.ServerName = addr.Host() + tlsConfig.InsecureSkipVerify = d.insecure + return tlsConfig, nil } // Dial calls ssh.Dial directly. func (d directDial) Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { if d.tlsRoutingEnabled { - client, err := dialALPNWithDeadline(network, addr, config, d.insecure) + client, err := d.dialALPNWithDeadline(network, addr, config) if err != nil { return nil, trace.Wrap(err) } @@ -114,11 +122,11 @@ func (d directDial) DialTimeout(network, address string, timeout time.Duration) if err != nil { return nil, trace.Wrap(err) } - tlsConn, err := tls.Dial("tcp", address, &tls.Config{ - NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)}, - InsecureSkipVerify: d.insecure, - ServerName: addr.Host(), - }) + conf, err := d.getTLSConfig(addr) + if err != nil { + return nil, trace.Wrap(err) + } + tlsConn, err := tls.Dial("tcp", address, conf) if err != nil { return nil, trace.Wrap(err) } @@ -134,10 +142,23 @@ func (d directDial) DialTimeout(network, address string, timeout time.Duration) type proxyDial struct { // proxyHost is the HTTPS proxy address. proxyHost string - // tlsRoutingEnabled indicates that proxy is running in TLSRouting mode. - tlsRoutingEnabled bool // insecure is whether to skip certificate validation. insecure bool + // tlsRoutingEnabled indicates that proxy is running in TLSRouting mode. + tlsRoutingEnabled bool + // tlsConfig is the TLS config to use. + tlsConfig *tls.Config +} + +// getTLSConfig configures the dialers TLS config for a specified address. +func (d proxyDial) getTLSConfig(addr *utils.NetAddr) (*tls.Config, error) { + if d.tlsConfig == nil { + return nil, trace.BadParameter("TLS config was nil") + } + tlsConfig := d.tlsConfig.Clone() + tlsConfig.ServerName = addr.Host() + tlsConfig.InsecureSkipVerify = d.insecure + return tlsConfig, nil } // DialTimeout acts like Dial but takes a timeout. @@ -149,7 +170,7 @@ func (d proxyDial) DialTimeout(network, address string, timeout time.Duration) ( defer cancel() ctx = timeoutCtx } - conn, err := dialProxy(ctx, d.proxyHost, address) + conn, err := apiclient.DialProxy(ctx, d.proxyHost, address) if err != nil { return nil, trace.Wrap(err) } @@ -158,11 +179,11 @@ func (d proxyDial) DialTimeout(network, address string, timeout time.Duration) ( if err != nil { return nil, trace.Wrap(err) } - conn = tls.Client(conn, &tls.Config{ - NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)}, - InsecureSkipVerify: d.insecure, - ServerName: address.Host(), - }) + conf, err := d.getTLSConfig(address) + if err != nil { + return nil, trace.Wrap(err) + } + conn = tls.Client(conn, conf) } return conn, nil } @@ -171,7 +192,7 @@ func (d proxyDial) DialTimeout(network, address string, timeout time.Duration) ( // SSH connection. func (d proxyDial) Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { // Build a proxy connection first. - pconn, err := dialProxy(context.Background(), d.proxyHost, addr) + pconn, err := apiclient.DialProxy(context.Background(), d.proxyHost, addr) if err != nil { return nil, trace.Wrap(err) } @@ -183,11 +204,11 @@ func (d proxyDial) Dial(network string, addr string, config *ssh.ClientConfig) ( if err != nil { return nil, trace.Wrap(err) } - pconn = tls.Client(pconn, &tls.Config{ - NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)}, - InsecureSkipVerify: d.insecure, - ServerName: address.Host(), - }) + conf, err := d.getTLSConfig(address) + if err != nil { + return nil, trace.Wrap(err) + } + pconn = tls.Client(pconn, conf) } // Do the same as ssh.Dial but pass in proxy connection. @@ -202,19 +223,22 @@ func (d proxyDial) Dial(network string, addr string, config *ssh.ClientConfig) ( } type dialerOptions struct { - // tlsRoutingEnabled indicates that proxy is running in TLSRouting mode. - tlsRoutingEnabled bool // insecureSkipTLSVerify is whether to skip certificate validation. insecureSkipTLSVerify bool + // tlsRoutingEnabled indicates that proxy is running in TLSRouting mode. + tlsRoutingEnabled bool + // tlsConfig is the TLS config to use for TLS routing. + tlsConfig *tls.Config } // DialerOptionFunc allows setting options as functional arguments to DialerFromEnvironment type DialerOptionFunc func(options *dialerOptions) // WithALPNDialer creates a dialer that allows to Teleport running in single-port mode. -func WithALPNDialer() DialerOptionFunc { +func WithALPNDialer(tlsConfig *tls.Config) DialerOptionFunc { return func(options *dialerOptions) { options.tlsRoutingEnabled = true + options.tlsConfig = tlsConfig } } @@ -231,7 +255,7 @@ func WithInsecureSkipTLSVerify(insecure bool) DialerOptionFunc { // server directly. func DialerFromEnvironment(addr string, opts ...DialerOptionFunc) Dialer { // Try and get proxy addr from the environment. - proxyAddr := getProxyAddress(addr) + proxyAddr := apiclient.GetProxyAddress(addr) var options dialerOptions for _, opt := range opts { @@ -243,129 +267,18 @@ func DialerFromEnvironment(addr string, opts ...DialerOptionFunc) Dialer { if proxyAddr == "" { log.Debugf("No proxy set in environment, returning direct dialer.") return directDial{ - tlsRoutingEnabled: options.tlsRoutingEnabled, insecure: options.insecureSkipTLSVerify, + tlsRoutingEnabled: options.tlsRoutingEnabled, + tlsConfig: options.tlsConfig, } } log.Debugf("Found proxy %q in environment, returning proxy dialer.", proxyAddr) return proxyDial{ proxyHost: proxyAddr, - tlsRoutingEnabled: options.tlsRoutingEnabled, insecure: options.insecureSkipTLSVerify, + tlsRoutingEnabled: options.tlsRoutingEnabled, + tlsConfig: options.tlsConfig, } } type DirectDialerOptFunc func(dial *directDial) - -func dialProxy(ctx context.Context, proxyAddr string, addr string) (net.Conn, error) { - var d net.Dialer - conn, err := d.DialContext(ctx, "tcp", proxyAddr) - if err != nil { - log.Warnf("Unable to dial to proxy: %v: %v.", proxyAddr, err) - return nil, trace.ConvertSystemError(err) - } - - connectReq := &http.Request{ - Method: http.MethodConnect, - URL: &url.URL{Opaque: addr}, - Host: addr, - Header: make(http.Header), - } - err = connectReq.Write(conn) - if err != nil { - log.Warnf("Unable to write to proxy: %v.", err) - return nil, trace.Wrap(err) - } - - // Read in the response. http.ReadResponse will read in the status line, mime - // headers, and potentially part of the response body. the body itself will - // not be read, but kept around so it can be read later. - br := bufio.NewReader(conn) - // Per the above comment, we're only using ReadResponse to check the status - // and then hand off the underlying connection to the caller. - // resp.Body.Close() would drain conn and close it, we don't need to do it - // here. Disabling bodyclose linter for this edge case. - //nolint:bodyclose - resp, err := http.ReadResponse(br, connectReq) - if err != nil { - conn.Close() - log.Warnf("Unable to read response: %v.", err) - return nil, trace.Wrap(err) - } - if resp.StatusCode != http.StatusOK { - conn.Close() - return nil, trace.BadParameter("unable to proxy connection: %v", resp.Status) - } - - // Return a bufferedConn that wraps a net.Conn and a *bufio.Reader. this - // needs to be done because http.ReadResponse will buffer part of the - // response body in the *bufio.Reader that was passed in. reads must first - // come from anything buffered, then from the underlying connection otherwise - // data will be lost. - return &bufferedConn{ - Conn: conn, - reader: br, - }, nil -} - -func getProxyAddress(addr string) string { - envs := []string{ - teleport.HTTPSProxy, - strings.ToLower(teleport.HTTPSProxy), - teleport.HTTPProxy, - strings.ToLower(teleport.HTTPProxy), - } - - for _, v := range envs { - envAddr := os.Getenv(v) - if envAddr == "" { - continue - } - proxyAddr, err := parse(envAddr) - if err != nil { - log.Debugf("Unable to parse environment variable %q: %q.", v, envAddr) - continue - } - log.Debugf("Successfully parsed environment variable %q: %q to %q.", v, envAddr, proxyAddr) - if !useProxy(addr) { - log.Debugf("Matched NO_PROXY override for %q: %q, going to ignore proxy variable.", v, envAddr) - return "" - } - return proxyAddr - } - - log.Debugf("No valid environment variables found.") - return "" -} - -// bufferedConn is used when part of the data on a connection has already been -// read by a *bufio.Reader. Reads will first try and read from the -// *bufio.Reader and when everything has been read, reads will go to the -// underlying connection. -type bufferedConn struct { - net.Conn - reader *bufio.Reader -} - -// Read first reads from the *bufio.Reader any data that has already been -// buffered. Once all buffered data has been read, reads go to the net.Conn. -func (bc *bufferedConn) Read(b []byte) (n int, err error) { - if bc.reader.Buffered() > 0 { - return bc.reader.Read(b) - } - return bc.Conn.Read(b) -} - -// parse will extract the host:port of the proxy to dial to. If the -// value is not prefixed by "http", then it will prepend "http" and try. -func parse(addr string) (string, error) { - proxyurl, err := url.Parse(addr) - if err != nil || !strings.HasPrefix(proxyurl.Scheme, "http") { - proxyurl, err = url.Parse("http://" + addr) - if err != nil { - return "", trace.Wrap(err) - } - } - - return proxyurl.Host, nil -} diff --git a/vendor/golang.org/x/net/http/httpproxy/proxy.go b/vendor/golang.org/x/net/http/httpproxy/proxy.go new file mode 100644 index 0000000000000..d2c8c87eab918 --- /dev/null +++ b/vendor/golang.org/x/net/http/httpproxy/proxy.go @@ -0,0 +1,368 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package httpproxy provides support for HTTP proxy determination +// based on environment variables, as provided by net/http's +// ProxyFromEnvironment function. +// +// The API is not subject to the Go 1 compatibility promise and may change at +// any time. +package httpproxy + +import ( + "errors" + "fmt" + "net" + "net/url" + "os" + "strings" + "unicode/utf8" + + "golang.org/x/net/idna" +) + +// Config holds configuration for HTTP proxy settings. See +// FromEnvironment for details. +type Config struct { + // HTTPProxy represents the value of the HTTP_PROXY or + // http_proxy environment variable. It will be used as the proxy + // URL for HTTP requests unless overridden by NoProxy. + HTTPProxy string + + // HTTPSProxy represents the HTTPS_PROXY or https_proxy + // environment variable. It will be used as the proxy URL for + // HTTPS requests unless overridden by NoProxy. + HTTPSProxy string + + // NoProxy represents the NO_PROXY or no_proxy environment + // variable. It specifies a string that contains comma-separated values + // specifying hosts that should be excluded from proxying. Each value is + // represented by an IP address prefix (1.2.3.4), an IP address prefix in + // CIDR notation (1.2.3.4/8), a domain name, or a special DNS label (*). + // An IP address prefix and domain name can also include a literal port + // number (1.2.3.4:80). + // A domain name matches that name and all subdomains. A domain name with + // a leading "." matches subdomains only. For example "foo.com" matches + // "foo.com" and "bar.foo.com"; ".y.com" matches "x.y.com" but not "y.com". + // A single asterisk (*) indicates that no proxying should be done. + // A best effort is made to parse the string and errors are + // ignored. + NoProxy string + + // CGI holds whether the current process is running + // as a CGI handler (FromEnvironment infers this from the + // presence of a REQUEST_METHOD environment variable). + // When this is set, ProxyForURL will return an error + // when HTTPProxy applies, because a client could be + // setting HTTP_PROXY maliciously. See https://golang.org/s/cgihttpproxy. + CGI bool +} + +// config holds the parsed configuration for HTTP proxy settings. +type config struct { + // Config represents the original configuration as defined above. + Config + + // httpsProxy is the parsed URL of the HTTPSProxy if defined. + httpsProxy *url.URL + + // httpProxy is the parsed URL of the HTTPProxy if defined. + httpProxy *url.URL + + // ipMatchers represent all values in the NoProxy that are IP address + // prefixes or an IP address in CIDR notation. + ipMatchers []matcher + + // domainMatchers represent all values in the NoProxy that are a domain + // name or hostname & domain name + domainMatchers []matcher +} + +// FromEnvironment returns a Config instance populated from the +// environment variables HTTP_PROXY, HTTPS_PROXY and NO_PROXY (or the +// lowercase versions thereof). HTTPS_PROXY takes precedence over +// HTTP_PROXY for https requests. +// +// The environment values may be either a complete URL or a +// "host[:port]", in which case the "http" scheme is assumed. An error +// is returned if the value is a different form. +func FromEnvironment() *Config { + return &Config{ + HTTPProxy: getEnvAny("HTTP_PROXY", "http_proxy"), + HTTPSProxy: getEnvAny("HTTPS_PROXY", "https_proxy"), + NoProxy: getEnvAny("NO_PROXY", "no_proxy"), + CGI: os.Getenv("REQUEST_METHOD") != "", + } +} + +func getEnvAny(names ...string) string { + for _, n := range names { + if val := os.Getenv(n); val != "" { + return val + } + } + return "" +} + +// ProxyFunc returns a function that determines the proxy URL to use for +// a given request URL. Changing the contents of cfg will not affect +// proxy functions created earlier. +// +// A nil URL and nil error are returned if no proxy is defined in the +// environment, or a proxy should not be used for the given request, as +// defined by NO_PROXY. +// +// As a special case, if req.URL.Host is "localhost" or a loopback address +// (with or without a port number), then a nil URL and nil error will be returned. +func (cfg *Config) ProxyFunc() func(reqURL *url.URL) (*url.URL, error) { + // Preprocess the Config settings for more efficient evaluation. + cfg1 := &config{ + Config: *cfg, + } + cfg1.init() + return cfg1.proxyForURL +} + +func (cfg *config) proxyForURL(reqURL *url.URL) (*url.URL, error) { + var proxy *url.URL + if reqURL.Scheme == "https" { + proxy = cfg.httpsProxy + } else if reqURL.Scheme == "http" { + proxy = cfg.httpProxy + if proxy != nil && cfg.CGI { + return nil, errors.New("refusing to use HTTP_PROXY value in CGI environment; see golang.org/s/cgihttpproxy") + } + } + if proxy == nil { + return nil, nil + } + if !cfg.useProxy(canonicalAddr(reqURL)) { + return nil, nil + } + + return proxy, nil +} + +func parseProxy(proxy string) (*url.URL, error) { + if proxy == "" { + return nil, nil + } + + proxyURL, err := url.Parse(proxy) + if err != nil || + (proxyURL.Scheme != "http" && + proxyURL.Scheme != "https" && + proxyURL.Scheme != "socks5") { + // proxy was bogus. Try prepending "http://" to it and + // see if that parses correctly. If not, we fall + // through and complain about the original one. + if proxyURL, err := url.Parse("http://" + proxy); err == nil { + return proxyURL, nil + } + } + if err != nil { + return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err) + } + return proxyURL, nil +} + +// useProxy reports whether requests to addr should use a proxy, +// according to the NO_PROXY or no_proxy environment variable. +// addr is always a canonicalAddr with a host and port. +func (cfg *config) useProxy(addr string) bool { + if len(addr) == 0 { + return true + } + host, port, err := net.SplitHostPort(addr) + if err != nil { + return false + } + if host == "localhost" { + return false + } + ip := net.ParseIP(host) + if ip != nil { + if ip.IsLoopback() { + return false + } + } + + addr = strings.ToLower(strings.TrimSpace(host)) + + if ip != nil { + for _, m := range cfg.ipMatchers { + if m.match(addr, port, ip) { + return false + } + } + } + for _, m := range cfg.domainMatchers { + if m.match(addr, port, ip) { + return false + } + } + return true +} + +func (c *config) init() { + if parsed, err := parseProxy(c.HTTPProxy); err == nil { + c.httpProxy = parsed + } + if parsed, err := parseProxy(c.HTTPSProxy); err == nil { + c.httpsProxy = parsed + } + + for _, p := range strings.Split(c.NoProxy, ",") { + p = strings.ToLower(strings.TrimSpace(p)) + if len(p) == 0 { + continue + } + + if p == "*" { + c.ipMatchers = []matcher{allMatch{}} + c.domainMatchers = []matcher{allMatch{}} + return + } + + // IPv4/CIDR, IPv6/CIDR + if _, pnet, err := net.ParseCIDR(p); err == nil { + c.ipMatchers = append(c.ipMatchers, cidrMatch{cidr: pnet}) + continue + } + + // IPv4:port, [IPv6]:port + phost, pport, err := net.SplitHostPort(p) + if err == nil { + if len(phost) == 0 { + // There is no host part, likely the entry is malformed; ignore. + continue + } + if phost[0] == '[' && phost[len(phost)-1] == ']' { + phost = phost[1 : len(phost)-1] + } + } else { + phost = p + } + // IPv4, IPv6 + if pip := net.ParseIP(phost); pip != nil { + c.ipMatchers = append(c.ipMatchers, ipMatch{ip: pip, port: pport}) + continue + } + + if len(phost) == 0 { + // There is no host part, likely the entry is malformed; ignore. + continue + } + + // domain.com or domain.com:80 + // foo.com matches bar.foo.com + // .domain.com or .domain.com:port + // *.domain.com or *.domain.com:port + if strings.HasPrefix(phost, "*.") { + phost = phost[1:] + } + matchHost := false + if phost[0] != '.' { + matchHost = true + phost = "." + phost + } + c.domainMatchers = append(c.domainMatchers, domainMatch{host: phost, port: pport, matchHost: matchHost}) + } +} + +var portMap = map[string]string{ + "http": "80", + "https": "443", + "socks5": "1080", +} + +// canonicalAddr returns url.Host but always with a ":port" suffix +func canonicalAddr(url *url.URL) string { + addr := url.Hostname() + if v, err := idnaASCII(addr); err == nil { + addr = v + } + port := url.Port() + if port == "" { + port = portMap[url.Scheme] + } + return net.JoinHostPort(addr, port) +} + +// Given a string of the form "host", "host:port", or "[ipv6::address]:port", +// return true if the string includes a port. +func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } + +func idnaASCII(v string) (string, error) { + // TODO: Consider removing this check after verifying performance is okay. + // Right now punycode verification, length checks, context checks, and the + // permissible character tests are all omitted. It also prevents the ToASCII + // call from salvaging an invalid IDN, when possible. As a result it may be + // possible to have two IDNs that appear identical to the user where the + // ASCII-only version causes an error downstream whereas the non-ASCII + // version does not. + // Note that for correct ASCII IDNs ToASCII will only do considerably more + // work, but it will not cause an allocation. + if isASCII(v) { + return v, nil + } + return idna.Lookup.ToASCII(v) +} + +func isASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] >= utf8.RuneSelf { + return false + } + } + return true +} + +// matcher represents the matching rule for a given value in the NO_PROXY list +type matcher interface { + // match returns true if the host and optional port or ip and optional port + // are allowed + match(host, port string, ip net.IP) bool +} + +// allMatch matches on all possible inputs +type allMatch struct{} + +func (a allMatch) match(host, port string, ip net.IP) bool { + return true +} + +type cidrMatch struct { + cidr *net.IPNet +} + +func (m cidrMatch) match(host, port string, ip net.IP) bool { + return m.cidr.Contains(ip) +} + +type ipMatch struct { + ip net.IP + port string +} + +func (m ipMatch) match(host, port string, ip net.IP) bool { + if m.ip.Equal(ip) { + return m.port == "" || m.port == port + } + return false +} + +type domainMatch struct { + host string + port string + + matchHost bool +} + +func (m domainMatch) match(host, port string, ip net.IP) bool { + if strings.HasSuffix(host, m.host) || (m.matchHost && host == m.host[1:]) { + return m.port == "" || m.port == port + } + return false +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 8a4e6aadb6d28..fec5da52ed719 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -869,6 +869,7 @@ golang.org/x/crypto/ssh/terminal golang.org/x/net/context golang.org/x/net/context/ctxhttp golang.org/x/net/http/httpguts +golang.org/x/net/http/httpproxy golang.org/x/net/http2 golang.org/x/net/http2/hpack golang.org/x/net/idna