net/tshttpproxy: don't proxy through ourselves

When running a SOCKS or HTTP proxy, configure the tshttpproxy package to
drop those addresses from any HTTP_PROXY or HTTPS_PROXY environment
variables.

Fixes #7407

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I6cd7cad7a609c639780484bad521c7514841764b
This commit is contained in:
Andrew Dunham
2023-03-04 21:49:05 -05:00
parent 62a1e9a44f
commit 38e4d303a2
5 changed files with 207 additions and 4 deletions

View File

@@ -9,11 +9,16 @@ import (
"context"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"sync"
"time"
"golang.org/x/net/http/httpproxy"
)
// InvalidateCache invalidates the package-level cache for ProxyFromEnvironment.
@@ -27,9 +32,24 @@ func InvalidateCache() {
var (
mu sync.Mutex
noProxyUntil time.Time // if non-zero, time at which ProxyFromEnvironment should check again
noProxyUntil time.Time // if non-zero, time at which ProxyFromEnvironment should check again
config *httpproxy.Config // used to create proxyFunc
proxyFunc func(*url.URL) (*url.URL, error)
)
func getProxyFunc() func(*url.URL) (*url.URL, error) {
// Create config/proxyFunc if it's not created
mu.Lock()
defer mu.Unlock()
if config == nil {
config = httpproxy.FromEnvironment()
}
if proxyFunc == nil {
proxyFunc = config.ProxyFunc()
}
return proxyFunc
}
// setNoProxyUntil stops calls to sysProxyEnv (if any) for the provided duration.
func setNoProxyUntil(d time.Duration) {
mu.Lock()
@@ -39,6 +59,59 @@ func setNoProxyUntil(d time.Duration) {
var _ = setNoProxyUntil // quiet staticcheck; Windows uses the above, more might later
// SetSelfProxy configures this package to avoid proxying through any of the
// provided addressese.g. if they refer to proxies being run by this process.
func SetSelfProxy(addrs ...string) {
mu.Lock()
defer mu.Unlock()
// Ensure we have a valid config
if config == nil {
config = httpproxy.FromEnvironment()
}
normalizeHostPort := func(s string) string {
host, portStr, err := net.SplitHostPort(s)
if err != nil {
return s
}
// Normalize the localhost IP into "localhost", to avoid IPv4/IPv6 confusion.
if host == "127.0.0.1" || host == "::1" {
return "localhost:" + portStr
}
// On Linux, all 127.0.0.1/8 IPs are also localhost.
if runtime.GOOS == "linux" && strings.HasPrefix(host, "127.0.0.") {
return "localhost:" + portStr
}
return s
}
normHTTP := normalizeHostPort(config.HTTPProxy)
normHTTPS := normalizeHostPort(config.HTTPSProxy)
// If any of our proxy variables point to one of the configured
// addresses, ignore them.
for _, addr := range addrs {
normAddr := normalizeHostPort(addr)
if normHTTP != "" && normHTTP == normAddr {
log.Printf("tshttpproxy: skipping HTTP_PROXY pointing to self: %q", addr)
config.HTTPProxy = ""
normHTTP = ""
}
if normHTTPS != "" && normHTTPS == normAddr {
log.Printf("tshttpproxy: skipping HTTPS_PROXY pointing to self: %q", addr)
config.HTTPSProxy = ""
normHTTPS = ""
}
}
// Invalidate to cause it to get re-created
proxyFunc = nil
}
// sysProxyFromEnv, if non-nil, specifies a platform-specific ProxyFromEnvironment
// func to use if http.ProxyFromEnvironment doesn't return a proxy.
// For example, WPAD PAC files on Windows.
@@ -48,7 +121,8 @@ var sysProxyFromEnv func(*http.Request) (*url.URL, error)
// but additionally does OS-specific proxy lookups if the environment variables
// alone don't specify a proxy.
func ProxyFromEnvironment(req *http.Request) (*url.URL, error) {
u, err := http.ProxyFromEnvironment(req)
localProxyFunc := getProxyFunc()
u, err := localProxyFunc(req.URL)
if u != nil && err == nil {
return u, nil
}

View File

@@ -81,3 +81,127 @@ func TestProxyFromEnvironment_setNoProxyUntil(t *testing.T) {
}
}
func TestSetSelfProxy(t *testing.T) {
// Ensure we clean everything up at the end of our test
t.Cleanup(func() {
config = nil
proxyFunc = nil
})
testCases := []struct {
name string
env map[string]string
self []string
wantHTTP string
wantHTTPS string
}{
{
name: "no self proxy",
env: map[string]string{
"HTTP_PROXY": "127.0.0.1:1234",
"HTTPS_PROXY": "127.0.0.1:1234",
},
self: nil,
wantHTTP: "127.0.0.1:1234",
wantHTTPS: "127.0.0.1:1234",
},
{
name: "skip proxies",
env: map[string]string{
"HTTP_PROXY": "127.0.0.1:1234",
"HTTPS_PROXY": "127.0.0.1:5678",
},
self: []string{"127.0.0.1:1234", "127.0.0.1:5678"},
wantHTTP: "", // skipped
wantHTTPS: "", // skipped
},
{
name: "localhost normalization of env var",
env: map[string]string{
"HTTP_PROXY": "localhost:1234",
"HTTPS_PROXY": "[::1]:5678",
},
self: []string{"127.0.0.1:1234", "127.0.0.1:5678"},
wantHTTP: "", // skipped
wantHTTPS: "", // skipped
},
{
name: "localhost normalization of addr",
env: map[string]string{
"HTTP_PROXY": "127.0.0.1:1234",
"HTTPS_PROXY": "127.0.0.1:1234",
},
self: []string{"[::1]:1234"},
wantHTTP: "", // skipped
wantHTTPS: "", // skipped
},
{
name: "no ports",
env: map[string]string{
"HTTP_PROXY": "myproxy",
"HTTPS_PROXY": "myproxy",
},
self: []string{"127.0.0.1:1234"},
wantHTTP: "myproxy",
wantHTTPS: "myproxy",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.env {
oldEnv, found := os.LookupEnv(k)
if found {
t.Cleanup(func() {
os.Setenv(k, oldEnv)
})
}
os.Setenv(k, v)
}
// Reset computed variables
config = nil
proxyFunc = func(*url.URL) (*url.URL, error) {
panic("should not be called")
}
SetSelfProxy(tt.self...)
if got := config.HTTPProxy; got != tt.wantHTTP {
t.Errorf("got HTTPProxy=%q; want %q", got, tt.wantHTTP)
}
if got := config.HTTPSProxy; got != tt.wantHTTPS {
t.Errorf("got HTTPSProxy=%q; want %q", got, tt.wantHTTPS)
}
if proxyFunc != nil {
t.Errorf("wanted nil proxyFunc")
}
// Verify that we do actually proxy through the
// expected proxy, if we have one configured.
pf := getProxyFunc()
if tt.wantHTTP != "" {
want := "http://" + tt.wantHTTP
uu, _ := url.Parse("http://tailscale.com")
dest, err := pf(uu)
if err != nil {
t.Error(err)
} else if dest.String() != want {
t.Errorf("got dest=%q; want %q", dest, want)
}
}
if tt.wantHTTPS != "" {
want := "http://" + tt.wantHTTPS
uu, _ := url.Parse("https://tailscale.com")
dest, err := pf(uu)
if err != nil {
t.Error(err)
} else if dest.String() != want {
t.Errorf("got dest=%q; want %q", dest, want)
}
}
})
}
}