tailscale/net/tshttpproxy/tshttpproxy.go
Andrew Dunham 38e4d303a2 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
2023-03-29 17:09:45 -04:00

196 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package tshttpproxy contains Tailscale additions to httpproxy not available
// in golang.org/x/net/http/httpproxy. Notably, it aims to support Windows better.
package tshttpproxy
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.
//
// It's intended to be called on network link/routing table changes.
func InvalidateCache() {
mu.Lock()
defer mu.Unlock()
noProxyUntil = time.Time{}
}
var (
mu sync.Mutex
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()
defer mu.Unlock()
noProxyUntil = time.Now().Add(d)
}
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.
var sysProxyFromEnv func(*http.Request) (*url.URL, error)
// ProxyFromEnvironment is like the standard library's http.ProxyFromEnvironment
// 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) {
localProxyFunc := getProxyFunc()
u, err := localProxyFunc(req.URL)
if u != nil && err == nil {
return u, nil
}
mu.Lock()
noProxyTime := noProxyUntil
mu.Unlock()
if time.Now().Before(noProxyTime) {
return nil, nil
}
if sysProxyFromEnv != nil {
u, err := sysProxyFromEnv(req)
if u != nil && err == nil {
return u, nil
}
}
return nil, err
}
var sysAuthHeader func(*url.URL) (string, error)
// GetAuthHeader returns the Authorization header value to send to proxy u.
func GetAuthHeader(u *url.URL) (string, error) {
if fake := os.Getenv("TS_DEBUG_FAKE_PROXY_AUTH"); fake != "" {
return fake, nil
}
if user := u.User.Username(); user != "" {
pass, ok := u.User.Password()
if !ok {
return "", nil
}
req := &http.Request{Header: make(http.Header)}
req.SetBasicAuth(user, pass)
return req.Header.Get("Authorization"), nil
}
if sysAuthHeader != nil {
return sysAuthHeader(u)
}
return "", nil
}
const proxyAuthHeader = "Proxy-Authorization"
// SetTransportGetProxyConnectHeader sets the provided Transport's
// GetProxyConnectHeader field, and adds logging of the received response.
func SetTransportGetProxyConnectHeader(tr *http.Transport) {
tr.GetProxyConnectHeader = func(ctx context.Context, proxyURL *url.URL, target string) (http.Header, error) {
v, err := GetAuthHeader(proxyURL)
if err != nil {
log.Printf("failed to get proxy Auth header for %v; ignoring: %v", proxyURL, err)
return nil, nil
}
if v == "" {
return nil, nil
}
return http.Header{proxyAuthHeader: []string{v}}, nil
}
tr.OnProxyConnectResponse = func(ctx context.Context, proxyURL *url.URL, connectReq *http.Request, res *http.Response) error {
auth := connectReq.Header.Get(proxyAuthHeader)
const truncLen = 20
if len(auth) > truncLen {
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
}
log.Printf("tshttpproxy: CONNECT response from %v for target %q (auth %q): %v", proxyURL, connectReq.Host, auth, res.Status)
return nil
}
}