mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 16:53:57 +00:00
Respect HTTP_PROXY/HTTPS_PROXY (#10209)
This change allows tsh to use HTTP proxies when HTTP_PROXY/HTTPS_PROXY is set in the environment.
This commit is contained in:
parent
6277ef8620
commit
4543bfd98d
|
@ -288,7 +288,7 @@ type (
|
|||
|
||||
// authConnect connects to the Teleport Auth Server directly.
|
||||
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)
|
||||
|
|
|
@ -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 {
|
||||
|
@ -73,7 +85,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 {
|
||||
|
|
|
@ -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
|
150
api/client/proxy.go
Normal file
150
api/client/proxy.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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"
|
||||
|
@ -81,6 +82,9 @@ func newWebClient(cfg *Config) (*http.Client, error) {
|
|||
RootCAs: cfg.Pool,
|
||||
InsecureSkipVerify: cfg.Insecure,
|
||||
},
|
||||
Proxy: func(req *http.Request) (*url.URL, error) {
|
||||
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -290,3 +290,36 @@ func TestExtract(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWebClientRespectHTTPProxy(t *testing.T) {
|
||||
t.Setenv("HTTPS_PROXY", "fakeproxy.example.com:9999")
|
||||
client, err := newWebClient(&Config{
|
||||
Context: context.Background(),
|
||||
ProxyAddr: "localhost:3080",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// 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, err := newWebClient(&Config{
|
||||
Context: context.Background(),
|
||||
ProxyAddr: "localhost:3080",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
//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")
|
||||
}
|
||||
|
|
|
@ -189,3 +189,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"
|
||||
)
|
||||
|
|
12
constants.go
12
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
|
||||
|
|
|
@ -129,7 +129,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)
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
@ -2288,37 +2289,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) {
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
73
lib/client/https_client_test.go
Normal file
73
lib/client/https_client_test.go
Normal file
|
@ -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")
|
||||
}
|
|
@ -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"
|
||||
|
@ -285,7 +287,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 {
|
||||
|
|
|
@ -18,6 +18,7 @@ package reversetunnel
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -33,6 +34,7 @@ import (
|
|||
"github.com/gravitational/teleport/api/utils/sshutils"
|
||||
"github.com/gravitational/teleport/lib/auth"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/teleport/lib/utils/proxy"
|
||||
|
||||
|
@ -97,7 +99,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...)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue