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

NO_PROXY port support + special case for proxying via localhost #11403

Merged
merged 54 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
54 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
2117a9b
Update NO_PROXY matching
atburke Mar 16, 2022
5d69d45
Change GetProxyAddress return type
atburke Mar 17, 2022
b3f1d13
Add ProxyAwareRoundTripper
atburke Mar 21, 2022
2a6b7b2
Remove siddontang logger from proxy.go
atburke Mar 22, 2022
bdddc69
Remove siddontang logger from proxy.go
atburke Mar 22, 2022
a23bf82
Merge master
atburke Mar 23, 2022
c4ab70a
Revert "Merge master"
atburke Mar 23, 2022
dc2d200
Merge branch 'master' into atburke/no-proxy-with-ports
atburke Mar 23, 2022
eaee3c5
Merge branch 'atburke/no-proxy-with-ports' into atburke/tsh-http-fall…
atburke Mar 23, 2022
c3946d7
Don't ignore roundtrip response in proxy test
atburke Mar 23, 2022
f4c1115
Revert proxyaddr type change in some places
atburke Mar 23, 2022
30e2eec
Revert more proxyaddr type changes
atburke Mar 23, 2022
ba1a6a0
Address review comments
atburke Mar 24, 2022
cf178f6
Fix transport type
atburke Mar 24, 2022
7a355b5
Precompute isHTTPLocalhost + refactor
atburke Mar 28, 2022
b38c7e4
Fix proxy check name
atburke Mar 28, 2022
f179048
Always return original error in parse
atburke Mar 28, 2022
1153ba4
Add nolint:bodyclose to test
atburke Mar 29, 2022
1dff5d4
Address more review feedback
atburke Mar 29, 2022
8b257b5
Fix handling scheme-less urls in parse
atburke Mar 30, 2022
c3deb88
Fix errors returned from parse
atburke Apr 1, 2022
0c9b8f7
Merge branch 'master' into atburke/tsh-http-fallback
atburke Apr 1, 2022
9d49c88
Merge branch 'master' into atburke/tsh-http-fallback
atburke Apr 4, 2022
1ea69be
Fix TwoClustersProxy integration test
atburke Apr 4, 2022
91e4fbe
Merge branch 'master' into atburke/tsh-http-fallback
atburke Apr 4, 2022
b5f595b
Fix ALPN integration tests
atburke Apr 4, 2022
7eda39f
Merge branch 'atburke/tsh-http-fallback' of github.com:gravitational/…
atburke Apr 4, 2022
7bce606
Merge branch 'master' into atburke/tsh-http-fallback
atburke Apr 4, 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
5 changes: 3 additions & 2 deletions api/client/contextdialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"net"
"time"

"github.com/gravitational/teleport/api/client/proxy"
"github.com/gravitational/teleport/api/client/webclient"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/utils/sshutils"
Expand Down Expand Up @@ -57,8 +58,8 @@ func newDirectDialer(keepAlivePeriod, dialTimeout time.Duration) ContextDialer {
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)
if proxyAddr := proxy.GetProxyAddress(addr); proxyAddr != nil {
return DialProxyWithDialer(ctx, proxyAddr.Host, addr, dialer)
}
return dialer.DialContext(ctx, network, addr)
})
Expand Down
74 changes: 0 additions & 74 deletions api/client/noproxy.go

This file was deleted.

48 changes: 0 additions & 48 deletions api/client/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ import (
"net"
"net/http"
"net/url"
"os"
"strings"

"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/trace"
log "github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -86,37 +83,6 @@ func DialProxyWithDialer(ctx context.Context, proxyAddr, addr string, dialer Con
}, 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
Expand All @@ -134,17 +100,3 @@ func (bc *bufferedConn) Read(b []byte) (n int, err error) {
}
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
}
95 changes: 95 additions & 0 deletions api/client/proxy/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
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 proxy
atburke marked this conversation as resolved.
Show resolved Hide resolved

import (
"net/http"
"net/url"
"strings"

"golang.org/x/net/http/httpproxy"
)

// GetProxyAddress gets the HTTP proxy address to use for a given address, if any.
func GetProxyAddress(dialAddr string) *url.URL {
addrURL, err := parse(dialAddr)
if err != nil {
return nil
}
proxyFunc := httpproxy.FromEnvironment().ProxyFunc()
if addrURL.Scheme != "" {
proxyURL, err := proxyFunc(addrURL)
if err != nil {
return nil
}
return proxyURL
}

for _, scheme := range []string{"https", "http"} {
addrURL.Scheme = scheme
atburke marked this conversation as resolved.
Show resolved Hide resolved
proxyURL, err := proxyFunc(addrURL)
if err == nil && proxyURL != nil {
return proxyURL
}
}

return nil
}

// parse parses a URL. If the address does not have a scheme, it will prepend "http" and try.
func parse(addr string) (*url.URL, error) {
proxyurl, err := url.Parse(addr)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Some things about this function are still unclear to me:

  • proxyurl is still confusing because this function doesn't actually deal with proxy addresses now.
  • In which case addr will not have a scheme in it?
  • Shouldn't we default to https if the scheme is empty? That would be a more secure default, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whether or not addr has a scheme depends on the caller; the existing proxy tests didn't have schemes, so I'm assuming that's a common and expected scenario.

The extra http:// is only there to help parse the url; parse doesn't assume http. GetProxyAddress handles the scheme-less URLs by checking https first, then http.

// Some URLs will fail to parse without a scheme (for example, <ip-address>:<port>), so try
// to parse again with a scheme. If that fails, return the original error.
if err != nil || !strings.HasPrefix(proxyurl.Scheme, "http") {
if proxyurl, err := url.Parse("http://" + addr); err == nil {
proxyurl.Scheme = ""
return proxyurl, nil
}
return nil, err
}
return proxyurl, nil
}

// HTTPFallbackRoundTripper is a wrapper for http.Transport that downgrades requests
// to plain HTTP when using a plain HTTP proxy at localhost.
type HTTPFallbackRoundTripper struct {
*http.Transport
jimbishopp marked this conversation as resolved.
Show resolved Hide resolved
isProxyHTTPLocalhost bool
}

func NewHTTPFallbackRoundTripper(transport *http.Transport, insecure bool) *HTTPFallbackRoundTripper {
atburke marked this conversation as resolved.
Show resolved Hide resolved
proxyConfig := httpproxy.FromEnvironment()
rt := HTTPFallbackRoundTripper{
Transport: transport,
isProxyHTTPLocalhost: strings.HasPrefix(proxyConfig.HTTPProxy, "http://localhost"),
}
if rt.TLSClientConfig != nil {
rt.TLSClientConfig.InsecureSkipVerify = insecure
}
return &rt
}

// RoundTrip executes a single HTTP transaction. Part of the RoundTripper interface.
func (rt *HTTPFallbackRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
tlsConfig := rt.Transport.TLSClientConfig
// Use plain HTTP if proxying via http://localhost in insecure mode.
if rt.isProxyHTTPLocalhost && tlsConfig != nil && tlsConfig.InsecureSkipVerify {
req.URL.Scheme = "http"
}
return rt.Transport.RoundTrip(req)
}
47 changes: 45 additions & 2 deletions api/client/proxy_test.go → api/client/proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package client
package proxy

import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"testing"

"github.com/stretchr/testify/require"
"golang.org/x/net/http/httpproxy"
)

func TestGetProxyAddress(t *testing.T) {
Expand Down Expand Up @@ -82,6 +86,24 @@ func TestGetProxyAddress(t *testing.T) {
proxyAddr: "",
targetAddr: "bla.example.com:1234",
},
{
info: "valid, no_proxy blocks matching port",
env: []env{
{name: "https_proxy", val: "proxy:9999"},
{name: "no_proxy", val: "example.com:1234"},
},
proxyAddr: "",
targetAddr: "example.com:1234",
},
{
info: "valid, no_proxy matches host but not port",
env: []env{
{name: "https_proxy", val: "proxy:9999"},
{name: "no_proxy", val: "example.com:1234"},
},
proxyAddr: "proxy:9999",
targetAddr: "example.com:5678",
},
}

for i, tt := range tests {
Expand All @@ -90,7 +112,28 @@ func TestGetProxyAddress(t *testing.T) {
t.Setenv(env.name, env.val)
}
p := GetProxyAddress(tt.targetAddr)
require.Equal(t, tt.proxyAddr, p)
if tt.proxyAddr == "" {
require.Nil(t, p)
} else {
require.Equal(t, tt.proxyAddr, p.Host)
}
})
}
}

func TestProxyAwareRoundTripper(t *testing.T) {
t.Setenv("HTTP_PROXY", "http://localhost:8888")
transport := &http.Transport{
TLSClientConfig: &tls.Config{},
Proxy: func(req *http.Request) (*url.URL, error) {
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
},
}
rt := NewHTTPFallbackRoundTripper(transport, true)
req, err := http.NewRequest(http.MethodGet, "https://localhost:9999", nil)
require.NoError(t, err)
// Don't care about response, only if the scheme changed.
_, err = rt.RoundTrip(req)
atburke marked this conversation as resolved.
Show resolved Hide resolved
require.Error(t, err)
require.Equal(t, "http", req.URL.Scheme)
}
19 changes: 10 additions & 9 deletions api/client/webclient/webclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"strconv"
"strings"

"github.com/gravitational/teleport/api/client/proxy"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/utils"
Expand Down Expand Up @@ -76,16 +77,16 @@ func newWebClient(cfg *Config) (*http.Client, error) {
if err := cfg.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: cfg.Pool,
InsecureSkipVerify: cfg.Insecure,
},
Proxy: func(req *http.Request) (*url.URL, error) {
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
},
transport := http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: cfg.Pool,
atburke marked this conversation as resolved.
Show resolved Hide resolved
},
Proxy: func(req *http.Request) (*url.URL, error) {
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
},
}
return &http.Client{
Transport: proxy.NewHTTPFallbackRoundTripper(&transport, cfg.Insecure),
}, nil
}

Expand Down
16 changes: 8 additions & 8 deletions lib/client/https_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"net/url"

"github.com/gravitational/teleport"
apiproxy "github.com/gravitational/teleport/api/client/proxy"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/utils"
Expand All @@ -37,16 +38,15 @@ func NewInsecureWebClient() *http.Client {
// Because Teleport clients can't be configured (yet), they take the default
// list of cipher suites from Go.
tlsConfig := utils.TLSConfig(nil)
tlsConfig.InsecureSkipVerify = true

return &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: func(req *http.Request) (*url.URL, error) {
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
},
transport := http.Transport{
TLSClientConfig: tlsConfig,
Proxy: func(req *http.Request) (*url.URL, error) {
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
},
}
return &http.Client{
Transport: apiproxy.NewHTTPFallbackRoundTripper(&transport, true),
atburke marked this conversation as resolved.
Show resolved Hide resolved
}
}

func newClientWithPool(pool *x509.CertPool) *http.Client {
Expand Down
Loading