Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect HTTP_PROXY/HTTPS_PROXY #10209

Merged
merged 42 commits into from
Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
72459fb
Add http proxy to web clients
atburke Feb 7, 2022
05fa8b5
Add tests
atburke Feb 8, 2022
37cd786
Reduce arg ambiguity
atburke Feb 9, 2022
e2df595
Make error assertions more specific
atburke Feb 9, 2022
8281dfc
Fix linting
atburke Feb 9, 2022
59088d7
Merge branch 'master' into atburke/tsh-https-proxy
atburke Feb 9, 2022
700abc2
Add messages to assertions
atburke Feb 9, 2022
d41122a
Change fake proxy address in tests
atburke Feb 9, 2022
d225e71
Add http proxy support for ssh
atburke Feb 10, 2022
810d958
Move proxy utils to api
atburke Feb 15, 2022
dd5e232
Add proxy-aware context dialer
atburke Feb 15, 2022
7981450
Fix incorrect address in makeProxySSHClient
atburke Feb 16, 2022
726297f
Merge branch 'master' into atburke/tsh-https-proxy
atburke Feb 19, 2022
1823b90
Merge branch 'master' into atburke/tsh-https-proxy
atburke Feb 22, 2022
f57114d
Merge branch 'master' into atburke/tsh-https-proxy
atburke Mar 1, 2022
ad44ccf
Add docs
atburke Mar 1, 2022
de6c715
Add NO_PROXY tests
atburke Mar 1, 2022
df6c5e2
Simplify bodyclose workaround
atburke Mar 2, 2022
bde8fb2
Stop caching env vars
atburke Mar 3, 2022
bbc3af7
Split makeProxySSHClient
atburke Mar 3, 2022
79e3f3f
Remove leftover teleport imports from api
atburke Mar 10, 2022
fcf43ee
Make dialALPNWithDeadline a member of directDial
atburke Mar 11, 2022
e24d3d0
Add DialProxyWithDialer for custom dialers
atburke Mar 11, 2022
749d889
Make tlsConfig mandatory
atburke Mar 14, 2022
4e2a20a
Bring back tlsRoutingEnabled flag
atburke Mar 14, 2022
cad2d6c
Move tls config configuration to its own function
atburke Mar 15, 2022
0edbc1d
Merge branch 'master' into atburke/tsh-https-proxy
atburke Mar 21, 2022
df2853e
Remove siddontang logger from proxy.go
atburke Mar 22, 2022
926f7bb
Merge branch 'master' into atburke/tsh-https-proxy
atburke Mar 22, 2022
23fdd82
Address review comments
atburke Mar 22, 2022
a94ce4b
Fix error message checks in tests
atburke Mar 22, 2022
dd2cb23
Fix missed url update
atburke Mar 22, 2022
2815f4c
Fix no_proxy value in tests
atburke Mar 22, 2022
5238d5a
Merge branch 'master' into atburke/tsh-https-proxy
atburke Mar 23, 2022
4f14ec4
Merge branch 'master' into atburke/tsh-https-proxy
atburke Mar 23, 2022
b343ff8
Tweak newWebClient arg description
atburke Mar 23, 2022
0774cc4
Merge branch 'atburke/tsh-https-proxy' of github.com:gravitational/te…
atburke Mar 23, 2022
b6dff62
Fix newWebClient signature in tests
atburke Mar 23, 2022
b861f75
Merge branch 'master' into atburke/tsh-https-proxy
atburke Mar 23, 2022
f5b3b71
Merge branch 'master' into atburke/tsh-https-proxy
atburke Mar 23, 2022
e127340
Merge branch 'master' into atburke/tsh-https-proxy
atburke Mar 23, 2022
76dfcbe
Merge branch 'master' into atburke/tsh-https-proxy
atburke Mar 23, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/client/webclient/webclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func newWebClient(insecure bool, pool *x509.CertPool) *http.Client {
RootCAs: pool,
InsecureSkipVerify: insecure,
},
Proxy: http.ProxyFromEnvironment,
},
}
}
Expand Down
15 changes: 15 additions & 0 deletions api/client/webclient/webclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,18 @@ 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, err := client.Get("https://example.com")
defer func() {
if resp != nil {
resp.Body.Close()
}
}()
// 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(), "no such host")
}
40 changes: 12 additions & 28 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -2198,37 +2199,20 @@ 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) {
var opts []proxy.DialerOptionFunc
if tc.Config.TLSRoutingEnabled {
return makeProxySSHClientWithTLSWrapper(tc, sshConfig)
}
client, err := ssh.Dial("tcp", tc.Config.SSHProxyAddr, sshConfig)
if err != nil {
return nil, trace.Wrap(err, "failed to authenticate with proxy %v", tc.Config.SSHProxyAddr)
tlsConfig, err := tc.loadTLSConfig()
if err != nil {
return nil, trace.Wrap(err)
}
tlsConfig.NextProtos = []string{string(alpncommon.ProtocolProxySSH)}
tlsConfig.InsecureSkipVerify = tc.Config.InsecureSkipVerify
opts = append(opts, proxy.WithALPNDialer(), proxy.WithTLSConfig(tlsConfig))
}
return client, nil
dialer := proxy.DialerFromEnvironment(tc.Config.WebProxyAddr, opts...)
client, err := dialer.Dial("tcp", tc.Config.SSHProxyAddr, sshConfig)
return client, trace.Wrap(err, "failed to authenticate with proxy %v", tc.Config.SSHProxyAddr)
}

func (tc *TeleportClient) rootClusterName() (string, error) {
Expand Down
2 changes: 2 additions & 0 deletions lib/client/https_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func NewInsecureWebClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
},
}
}
Expand All @@ -54,6 +55,7 @@ func newClientWithPool(pool *x509.CertPool) *http.Client {
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
},
}
}
Expand Down
53 changes: 53 additions & 0 deletions lib/client/https_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
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, err := client.Get("https://example.com")
defer func() {
if resp != nil {
resp.Body.Close()
}
}()
// 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(), "no such host")
}

func TestNewClientWithPoolHTTPProxy(t *testing.T) {
t.Setenv("HTTPS_PROXY", "fakeproxy.example.com:9999")
client := newClientWithPool(nil)
resp, err := client.Get("https://example.com")
defer func() {
if resp != nil {
resp.Body.Close()
}
}()
// 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(), "no such host")
}
78 changes: 52 additions & 26 deletions lib/utils/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,23 @@ 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) (*ssh.Client, error) {
func dialALPNWithDeadline(network string, addr string, config *ssh.ClientConfig, tlsConfig *tls.Config) (*ssh.Client, error) {
dialer := &net.Dialer{
Timeout: config.Timeout,
}
address, err := utils.ParseAddr(addr)
if err != nil {
return nil, trace.Wrap(err)
}
tlsConn, err := tls.DialWithDialer(dialer, network, addr, &tls.Config{
NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)},
InsecureSkipVerify: lib.IsInsecureDevMode(),
ServerName: address.Host(),
})
conf := tlsConfig.Clone()
if conf == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can conf be nil here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conf can be nil when the caller doesn't use proxy.WithTLSConfig() to use a custom tls config.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I'm not sure I fully understand this tlsConfig logic. Why are we setting NextProtos to ProtocolReverseTunnel here in what (I think) is supposed to be a generic proxy, but only in the case when tlsConfig wasn't passed in?

@smallinsky Can you take a look at these changes too pls - do you remember why we're setting ProtocolReverseTunnel here? Is this proxy supposed to be used by the agents only? Just want to make sure we don't break any scenarios.

Is this proxy is now used by both reverse tunnel agents and tsh, I wonder if we should make the TLS config mandatory and have the caller pass it appropriately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProtocolReverseTunnel is the default from before the tls config was injected. As long as it doesn't break anything, I think making tls config mandatory is the way to go.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you remember why we're setting ProtocolReverseTunnel here?

The name of the dialer is misleading. It is used in TunnelAuthDialer and Reverse tunnel Agent call so under the hood it is Reverse Tunnel Dialer where a connection is established to Reverse tunnel service thus in case of TLSRouting mode the alpncommon.ProtocolReverseTunnel protocol is set.

conf = &tls.Config{
NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)},
InsecureSkipVerify: lib.IsInsecureDevMode(),
}
}
conf.ServerName = address.Host()
tlsConn, err := tls.DialWithDialer(dialer, network, addr, conf)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -86,13 +90,14 @@ type Dialer interface {
}

type directDial struct {
useTLS bool
useTLS bool
tlsConfig *tls.Config
}

// Dial calls ssh.Dial directly.
func (d directDial) Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
if d.useTLS {
client, err := dialALPNWithDeadline(network, addr, config)
client, err := dialALPNWithDeadline(network, addr, config, d.tlsConfig)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -112,11 +117,15 @@ 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: lib.IsInsecureDevMode(),
ServerName: addr.Host(),
})
conf := d.tlsConfig.Clone()
if conf == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can conf be nil here?

conf = &tls.Config{
NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)},
InsecureSkipVerify: lib.IsInsecureDevMode(),
}
}
conf.ServerName = addr.Host()
tlsConn, err := tls.Dial("tcp", address, conf)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -132,6 +141,7 @@ func (d directDial) DialTimeout(network, address string, timeout time.Duration)
type proxyDial struct {
proxyHost string
useTLS bool
tlsConfig *tls.Config
}

// DialTimeout acts like Dial but takes a timeout.
Expand All @@ -152,11 +162,15 @@ 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: lib.IsInsecureDevMode(),
ServerName: address.Host(),
})
conf := d.tlsConfig.Clone()
if conf == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same q as above.

conf = &tls.Config{
NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)},
InsecureSkipVerify: lib.IsInsecureDevMode(),
}
}
conf.ServerName = address.Host()
conn = tls.Client(conn, conf)
}
return conn, nil
}
Expand All @@ -177,11 +191,15 @@ 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: lib.IsInsecureDevMode(),
ServerName: address.Host(),
})
conf := d.tlsConfig.Clone()
if conf == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same q as above.

conf = &tls.Config{
NextProtos: []string{string(alpncommon.ProtocolReverseTunnel)},
InsecureSkipVerify: lib.IsInsecureDevMode(),
}
}
conf.ServerName = address.Host()
pconn = tls.Client(pconn, conf)
}

// Do the same as ssh.Dial but pass in proxy connection.
Expand All @@ -196,7 +214,8 @@ func (d proxyDial) Dial(network string, addr string, config *ssh.ClientConfig) (
}

type dialerOptions struct {
dialTLS bool
dialTLS bool
tlsConfig *tls.Config
}

// DialerOptionFunc allows setting options as functional arguments to DialerFromEnvironment
Expand All @@ -209,6 +228,13 @@ func WithALPNDialer() DialerOptionFunc {
}
}

// WithTLSConfig creates a dialer that uses a specific tls config.
func WithTLSConfig(tlsConfig *tls.Config) DialerOptionFunc {
return func(options *dialerOptions) {
options.tlsConfig = tlsConfig
}
}

// DialerFromEnvironment returns a Dial function. If the https_proxy or http_proxy
// environment variable are set, it returns a function that will dial through
// said proxy server. If neither variable is set, it will connect to the SSH
Expand All @@ -226,10 +252,10 @@ func DialerFromEnvironment(addr string, opts ...DialerOptionFunc) Dialer {
// otherwise return a proxy dialer.
if proxyAddr == "" {
log.Debugf("No proxy set in environment, returning direct dialer.")
return directDial{useTLS: options.dialTLS}
return directDial{useTLS: options.dialTLS, tlsConfig: options.tlsConfig}
}
log.Debugf("Found proxy %q in environment, returning proxy dialer.", proxyAddr)
return proxyDial{proxyHost: proxyAddr, useTLS: options.dialTLS}
return proxyDial{proxyHost: proxyAddr, useTLS: options.dialTLS, tlsConfig: options.tlsConfig}
}

type DirectDialerOptFunc func(dial *directDial)
Expand Down