mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			238 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			238 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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"
 | ||
| 	"errors"
 | ||
| 	"fmt"
 | ||
| 	"log"
 | ||
| 	"net"
 | ||
| 	"net/http"
 | ||
| 	"net/url"
 | ||
| 	"os"
 | ||
| 	"runtime"
 | ||
| 	"strings"
 | ||
| 	"sync"
 | ||
| 	"time"
 | ||
| 
 | ||
| 	"golang.org/x/net/http/httpproxy"
 | ||
| 	"tailscale.com/util/mak"
 | ||
| )
 | ||
| 
 | ||
| // 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)
 | ||
| )
 | ||
| 
 | ||
| // SetProxyFunc can be used by clients to set a platform-specific function for proxy resolution.
 | ||
| // If config is set when this function is called, an error will be returned.
 | ||
| // The provided function should return a proxy URL for the given request URL,
 | ||
| // nil if no proxy is enabled for the request URL, or an error if proxy settings cannot be resolved.
 | ||
| func SetProxyFunc(fn func(*url.URL) (*url.URL, error)) error {
 | ||
| 	mu.Lock()
 | ||
| 	defer mu.Unlock()
 | ||
| 
 | ||
| 	// Allow override only if config is not set
 | ||
| 	if config != nil {
 | ||
| 		return errors.New("tshttpproxy: SetProxyFunc can only be called when config is not set")
 | ||
| 	}
 | ||
| 
 | ||
| 	proxyFunc = fn
 | ||
| 	return nil
 | ||
| }
 | ||
| 
 | ||
| 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 addresses–e.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)
 | ||
| 
 | ||
| // These variables track whether we've printed a log message for a given proxy
 | ||
| // URL; we only print them once to avoid log spam.
 | ||
| var (
 | ||
| 	logMessageMu      sync.Mutex
 | ||
| 	logMessagePrinted map[string]bool
 | ||
| )
 | ||
| 
 | ||
| // 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) (ret *url.URL, _ error) {
 | ||
| 	defer func() {
 | ||
| 		if ret == nil {
 | ||
| 			return
 | ||
| 		}
 | ||
| 
 | ||
| 		ss := ret.String()
 | ||
| 
 | ||
| 		logMessageMu.Lock()
 | ||
| 		defer logMessageMu.Unlock()
 | ||
| 		if logMessagePrinted[ss] {
 | ||
| 			return
 | ||
| 		}
 | ||
| 		log.Printf("tshttpproxy: using proxy %q for URL: %q", ss, req.URL.String())
 | ||
| 		mak.Set(&logMessagePrinted, ss, true)
 | ||
| 	}()
 | ||
| 
 | ||
| 	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
 | ||
| 	}
 | ||
| }
 |