Skip to content

Commit

Permalink
NO_PROXY port support + special case for proxying via localhost (#11403)
Browse files Browse the repository at this point in the history
This change updates NO_PROXY handling to allow blocking specific host:port combinations, rather than just the host. It also adds a special case for downgrading requests to plain HTTP when --insecure is true and the request goes through a plain HTTP proxy at localhost (i.e. HTTP_PROXY=http://localhost).
  • Loading branch information
atburke authored Apr 4, 2022
1 parent dcad1b1 commit e3a8fb7
Show file tree
Hide file tree
Showing 13 changed files with 379 additions and 250 deletions.
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
}
102 changes: 102 additions & 0 deletions api/client/proxy/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
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

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

"github.com/gravitational/trace"
"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 || addrURL == 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
proxyURL, err := proxyFunc(addrURL)
if err == nil && proxyURL != nil {
return proxyURL
}
}

return nil
}

// parse parses an absolute URL. Unlike url.Parse, absolute URLs without a scheme are allowed.
func parse(addr string) (*url.URL, error) {
if addr == "" {
return nil, nil
}
addrURL, err := url.Parse(addr)
if err == nil && addrURL.Host != "" {
return addrURL, nil
}

// url.Parse won't correctly parse an absolute URL without a scheme, so try again with a scheme.
addrURL, err2 := url.Parse("http://" + addr)
if err2 != nil {
return nil, trace.NewAggregate(err, err2)
}
addrURL.Scheme = ""
return addrURL, 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
isProxyHTTPLocalhost bool
}

// NewHTTPFallbackRoundTripper creates a new initialized HTTP fallback roundtripper.
func NewHTTPFallbackRoundTripper(transport *http.Transport, insecure bool) *HTTPFallbackRoundTripper {
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)
}
Loading

0 comments on commit e3a8fb7

Please sign in to comment.