2023-01-27 21:37:20 +00:00
|
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2020-08-13 23:25:54 +01:00
|
|
|
|
|
|
|
|
|
// 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 (
|
2023-02-04 00:37:33 +00:00
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
2023-03-05 02:49:05 +00:00
|
|
|
|
"net"
|
2020-08-13 23:25:54 +01:00
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
2020-08-27 16:14:03 +01:00
|
|
|
|
"os"
|
2023-03-05 02:49:05 +00:00
|
|
|
|
"runtime"
|
|
|
|
|
"strings"
|
2020-09-21 22:02:58 +01:00
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
2023-03-05 02:49:05 +00:00
|
|
|
|
|
|
|
|
|
"golang.org/x/net/http/httpproxy"
|
2024-02-23 00:06:46 +00:00
|
|
|
|
"tailscale.com/util/mak"
|
2020-08-13 23:25:54 +01:00
|
|
|
|
)
|
|
|
|
|
|
2020-09-21 22:02:58 +01:00
|
|
|
|
// 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
|
2023-03-05 02:49:05 +00:00
|
|
|
|
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)
|
2020-09-21 22:02:58 +01:00
|
|
|
|
)
|
|
|
|
|
|
2023-03-05 02:49:05 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-13 17:58:47 +00:00
|
|
|
|
// setNoProxyUntil stops calls to sysProxyEnv (if any) for the provided duration.
|
2020-09-21 22:02:58 +01:00
|
|
|
|
func setNoProxyUntil(d time.Duration) {
|
|
|
|
|
mu.Lock()
|
|
|
|
|
defer mu.Unlock()
|
|
|
|
|
noProxyUntil = time.Now().Add(d)
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-21 23:01:30 +01:00
|
|
|
|
var _ = setNoProxyUntil // quiet staticcheck; Windows uses the above, more might later
|
|
|
|
|
|
2023-03-05 02:49:05 +00:00
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-13 23:25:54 +01:00
|
|
|
|
// 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)
|
|
|
|
|
|
2024-02-23 00:06:46 +00:00
|
|
|
|
// 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
|
|
|
|
|
)
|
|
|
|
|
|
2022-11-13 17:58:47 +00:00
|
|
|
|
// 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.
|
2024-02-23 00:06:46 +00:00
|
|
|
|
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)
|
|
|
|
|
}()
|
|
|
|
|
|
2023-03-05 02:49:05 +00:00
|
|
|
|
localProxyFunc := getProxyFunc()
|
|
|
|
|
u, err := localProxyFunc(req.URL)
|
2022-11-13 17:58:47 +00:00
|
|
|
|
if u != nil && err == nil {
|
|
|
|
|
return u, nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-21 22:02:58 +01:00
|
|
|
|
mu.Lock()
|
|
|
|
|
noProxyTime := noProxyUntil
|
|
|
|
|
mu.Unlock()
|
|
|
|
|
if time.Now().Before(noProxyTime) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-13 23:25:54 +01:00
|
|
|
|
if sysProxyFromEnv != nil {
|
|
|
|
|
u, err := sysProxyFromEnv(req)
|
|
|
|
|
if u != nil && err == nil {
|
|
|
|
|
return u, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2020-08-25 21:45:15 +01:00
|
|
|
|
|
|
|
|
|
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) {
|
2020-08-27 16:14:03 +01:00
|
|
|
|
if fake := os.Getenv("TS_DEBUG_FAKE_PROXY_AUTH"); fake != "" {
|
|
|
|
|
return fake, nil
|
|
|
|
|
}
|
2021-02-17 21:01:47 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
2021-02-18 05:23:39 +00:00
|
|
|
|
if sysAuthHeader != nil {
|
|
|
|
|
return sysAuthHeader(u)
|
|
|
|
|
}
|
2020-08-25 21:45:15 +01:00
|
|
|
|
return "", nil
|
|
|
|
|
}
|
2020-08-27 04:02:16 +01:00
|
|
|
|
|
2023-02-04 00:37:33 +00:00
|
|
|
|
const proxyAuthHeader = "Proxy-Authorization"
|
2020-08-27 04:02:16 +01:00
|
|
|
|
|
2022-09-25 19:29:55 +01:00
|
|
|
|
// SetTransportGetProxyConnectHeader sets the provided Transport's
|
2023-02-04 00:37:33 +00:00
|
|
|
|
// GetProxyConnectHeader field, and adds logging of the received response.
|
2020-08-27 04:02:16 +01:00
|
|
|
|
func SetTransportGetProxyConnectHeader(tr *http.Transport) {
|
2023-02-04 00:37:33 +00:00
|
|
|
|
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
|
2020-08-27 04:02:16 +01:00
|
|
|
|
}
|
|
|
|
|
}
|