mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 08:11:32 +01:00 
			
		
		
		
	Using WINHTTP_AUTOPROXY_ALLOW_AUTOCONFIG on Windows versions older than Windows 10 1703 (build 15063) is not supported and causes WinHttpGetProxyForUrl to fail with ERROR_INVALID_PARAMETER. This results in failures reaching the control on environments where a proxy is required. We use wingoes version detection to conditionally set the WINHTTP_AUTOPROXY_ALLOW_AUTOCONFIG flag on Windows builds greater than 15063. While there, we also update proxy detection to use WINHTTP_AUTO_DETECT_TYPE_DNS_A, as DNS-based proxy discovery might be required with Active Directory and in certain other environments. Updates tailscale/corp#29168 Fixes #879 Signed-off-by: Nick Khyl <nickk@tailscale.com>
		
			
				
	
	
		
			289 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| package tshttpproxy
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/base64"
 | |
| 	"fmt"
 | |
| 	"log"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"runtime"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"syscall"
 | |
| 	"time"
 | |
| 	"unsafe"
 | |
| 
 | |
| 	"github.com/alexbrainman/sspi/negotiate"
 | |
| 	"github.com/dblohm7/wingoes"
 | |
| 	"golang.org/x/sys/windows"
 | |
| 	"tailscale.com/hostinfo"
 | |
| 	"tailscale.com/syncs"
 | |
| 	"tailscale.com/types/logger"
 | |
| 	"tailscale.com/util/clientmetric"
 | |
| 	"tailscale.com/util/cmpver"
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	sysProxyFromEnv = proxyFromWinHTTPOrCache
 | |
| 	sysAuthHeader = sysAuthHeaderWindows
 | |
| }
 | |
| 
 | |
| var cachedProxy struct {
 | |
| 	sync.Mutex
 | |
| 	val *url.URL
 | |
| }
 | |
| 
 | |
| // proxyErrorf is a rate-limited logger specifically for errors asking
 | |
| // WinHTTP for the proxy information. We don't want to log about
 | |
| // errors often, otherwise the log message itself will generate a new
 | |
| // HTTP request which ultimately will call back into us to log again,
 | |
| // forever. So for errors, we only log a bit.
 | |
| var proxyErrorf = logger.RateLimitedFn(log.Printf, 10*time.Minute, 2 /* burst*/, 10 /* maxCache */)
 | |
| 
 | |
| var (
 | |
| 	metricSuccess              = clientmetric.NewCounter("winhttp_proxy_success")
 | |
| 	metricErrDetectionFailed   = clientmetric.NewCounter("winhttp_proxy_err_detection_failed")
 | |
| 	metricErrInvalidParameters = clientmetric.NewCounter("winhttp_proxy_err_invalid_param")
 | |
| 	metricErrDownloadScript    = clientmetric.NewCounter("winhttp_proxy_err_download_script")
 | |
| 	metricErrTimeout           = clientmetric.NewCounter("winhttp_proxy_err_timeout")
 | |
| 	metricErrOther             = clientmetric.NewCounter("winhttp_proxy_err_other")
 | |
| )
 | |
| 
 | |
| func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
 | |
| 	if req.URL == nil {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	urlStr := req.URL.String()
 | |
| 
 | |
| 	ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	type result struct {
 | |
| 		proxy *url.URL
 | |
| 		err   error
 | |
| 	}
 | |
| 	resc := make(chan result, 1)
 | |
| 	go func() {
 | |
| 		proxy, err := proxyFromWinHTTP(ctx, urlStr)
 | |
| 		resc <- result{proxy, err}
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case res := <-resc:
 | |
| 		err := res.err
 | |
| 		if err == nil {
 | |
| 			metricSuccess.Add(1)
 | |
| 			cachedProxy.Lock()
 | |
| 			defer cachedProxy.Unlock()
 | |
| 			if was, now := fmt.Sprint(cachedProxy.val), fmt.Sprint(res.proxy); was != now {
 | |
| 				log.Printf("tshttpproxy: winhttp: updating cached proxy setting from %v to %v", was, now)
 | |
| 			}
 | |
| 			cachedProxy.val = res.proxy
 | |
| 			return res.proxy, nil
 | |
| 		}
 | |
| 
 | |
| 		// See https://docs.microsoft.com/en-us/windows/win32/winhttp/error-messages
 | |
| 		const (
 | |
| 			ERROR_WINHTTP_AUTODETECTION_FAILED      = 12180
 | |
| 			ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT = 12167
 | |
| 		)
 | |
| 		if err == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
 | |
| 			metricErrDetectionFailed.Add(1)
 | |
| 			setNoProxyUntil(10 * time.Second)
 | |
| 			return nil, nil
 | |
| 		}
 | |
| 		if err == windows.ERROR_INVALID_PARAMETER {
 | |
| 			metricErrInvalidParameters.Add(1)
 | |
| 			setNoProxyUntil(10 * time.Second)
 | |
| 			proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): ERROR_INVALID_PARAMETER [unexpected]", urlStr)
 | |
| 			return nil, nil
 | |
| 		}
 | |
| 		proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): %v/%#v", urlStr, err, err)
 | |
| 		if err == syscall.Errno(ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT) {
 | |
| 			metricErrDownloadScript.Add(1)
 | |
| 			setNoProxyUntil(10 * time.Second)
 | |
| 			return nil, nil
 | |
| 		}
 | |
| 		metricErrOther.Add(1)
 | |
| 		return nil, err
 | |
| 	case <-ctx.Done():
 | |
| 		metricErrTimeout.Add(1)
 | |
| 		cachedProxy.Lock()
 | |
| 		defer cachedProxy.Unlock()
 | |
| 		proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): timeout; using cached proxy %v", urlStr, cachedProxy.val)
 | |
| 		return cachedProxy.val, nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func proxyFromWinHTTP(ctx context.Context, urlStr string) (proxy *url.URL, err error) {
 | |
| 	runtime.LockOSThread()
 | |
| 	defer runtime.UnlockOSThread()
 | |
| 
 | |
| 	whi, err := httpOpen()
 | |
| 	if err != nil {
 | |
| 		proxyErrorf("winhttp: Open: %v", err)
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer whi.Close()
 | |
| 
 | |
| 	t0 := time.Now()
 | |
| 	v, err := whi.GetProxyForURL(urlStr)
 | |
| 	td := time.Since(t0).Round(time.Millisecond)
 | |
| 	if err := ctx.Err(); err != nil {
 | |
| 		log.Printf("tshttpproxy: winhttp: context canceled, ignoring GetProxyForURL(%q) after %v", urlStr, td)
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if v == "" {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	// Discard all but first proxy value for now.
 | |
| 	if i := strings.Index(v, ";"); i != -1 {
 | |
| 		v = v[:i]
 | |
| 	}
 | |
| 	if !strings.HasPrefix(v, "https://") {
 | |
| 		v = "http://" + v
 | |
| 	}
 | |
| 	return url.Parse(v)
 | |
| }
 | |
| 
 | |
| var userAgent = windows.StringToUTF16Ptr("Tailscale")
 | |
| 
 | |
| const (
 | |
| 	winHTTP_ACCESS_TYPE_DEFAULT_PROXY   = 0
 | |
| 	winHTTP_ACCESS_TYPE_AUTOMATIC_PROXY = 4
 | |
| 	winHTTP_AUTOPROXY_ALLOW_AUTOCONFIG  = 0x00000100
 | |
| 	winHTTP_AUTOPROXY_AUTO_DETECT       = 1
 | |
| 	winHTTP_AUTO_DETECT_TYPE_DHCP       = 0x00000001
 | |
| 	winHTTP_AUTO_DETECT_TYPE_DNS_A      = 0x00000002
 | |
| )
 | |
| 
 | |
| // Windows 8.1 is actually Windows 6.3 under the hood. Yay, marketing!
 | |
| const win8dot1Ver = "6.3"
 | |
| 
 | |
| // accessType is the flag we must pass to WinHttpOpen for proxy resolution
 | |
| // depending on whether or not we're running Windows < 8.1
 | |
| var accessType syncs.AtomicValue[uint32]
 | |
| 
 | |
| func getAccessFlag() uint32 {
 | |
| 	if flag, ok := accessType.LoadOk(); ok {
 | |
| 		return flag
 | |
| 	}
 | |
| 	var flag uint32
 | |
| 	if cmpver.Compare(hostinfo.GetOSVersion(), win8dot1Ver) < 0 {
 | |
| 		flag = winHTTP_ACCESS_TYPE_DEFAULT_PROXY
 | |
| 	} else {
 | |
| 		flag = winHTTP_ACCESS_TYPE_AUTOMATIC_PROXY
 | |
| 	}
 | |
| 	accessType.Store(flag)
 | |
| 	return flag
 | |
| }
 | |
| 
 | |
| func httpOpen() (winHTTPInternet, error) {
 | |
| 	return winHTTPOpen(
 | |
| 		userAgent,
 | |
| 		getAccessFlag(),
 | |
| 		nil, /* WINHTTP_NO_PROXY_NAME */
 | |
| 		nil, /* WINHTTP_NO_PROXY_BYPASS */
 | |
| 		0,
 | |
| 	)
 | |
| }
 | |
| 
 | |
| type winHTTPInternet windows.Handle
 | |
| 
 | |
| func (hi winHTTPInternet) Close() error {
 | |
| 	return winHTTPCloseHandle(hi)
 | |
| }
 | |
| 
 | |
| // WINHTTP_AUTOPROXY_OPTIONS
 | |
| // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_autoproxy_options
 | |
| type winHTTPAutoProxyOptions struct {
 | |
| 	DwFlags                uint32
 | |
| 	DwAutoDetectFlags      uint32
 | |
| 	AutoConfigUrl          *uint16
 | |
| 	_                      uintptr
 | |
| 	_                      uint32
 | |
| 	FAutoLogonIfChallenged int32 // BOOL
 | |
| }
 | |
| 
 | |
| // WINHTTP_PROXY_INFO
 | |
| // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_proxy_info
 | |
| type winHTTPProxyInfo struct {
 | |
| 	AccessType  uint32
 | |
| 	Proxy       *uint16
 | |
| 	ProxyBypass *uint16
 | |
| }
 | |
| 
 | |
| type winHGlobal windows.Handle
 | |
| 
 | |
| func globalFreeUTF16Ptr(p *uint16) error {
 | |
| 	return globalFree((winHGlobal)(unsafe.Pointer(p)))
 | |
| }
 | |
| 
 | |
| func (pi *winHTTPProxyInfo) free() {
 | |
| 	if pi.Proxy != nil {
 | |
| 		globalFreeUTF16Ptr(pi.Proxy)
 | |
| 		pi.Proxy = nil
 | |
| 	}
 | |
| 	if pi.ProxyBypass != nil {
 | |
| 		globalFreeUTF16Ptr(pi.ProxyBypass)
 | |
| 		pi.ProxyBypass = nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var getProxyForURLOpts = sync.OnceValue(func() *winHTTPAutoProxyOptions {
 | |
| 	opts := &winHTTPAutoProxyOptions{
 | |
| 		DwFlags:           winHTTP_AUTOPROXY_AUTO_DETECT,
 | |
| 		DwAutoDetectFlags: winHTTP_AUTO_DETECT_TYPE_DHCP | winHTTP_AUTO_DETECT_TYPE_DNS_A,
 | |
| 	}
 | |
| 	// Support for the WINHTTP_AUTOPROXY_ALLOW_AUTOCONFIG flag was added in Windows 10, version 1703.
 | |
| 	//
 | |
| 	// Using it on earlier versions causes GetProxyForURL to fail with ERROR_INVALID_PARAMETER,
 | |
| 	// which prevents proxy detection and can lead to failures reaching the control server
 | |
| 	// on environments where a proxy is required.
 | |
| 	//
 | |
| 	// https://web.archive.org/web/20250529044903/https://learn.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_autoproxy_options
 | |
| 	if wingoes.IsWin10BuildOrGreater(wingoes.Win10Build1703) {
 | |
| 		opts.DwFlags |= winHTTP_AUTOPROXY_ALLOW_AUTOCONFIG
 | |
| 	}
 | |
| 	return opts
 | |
| })
 | |
| 
 | |
| func (hi winHTTPInternet) GetProxyForURL(urlStr string) (string, error) {
 | |
| 	var out winHTTPProxyInfo
 | |
| 	err := winHTTPGetProxyForURL(
 | |
| 		hi,
 | |
| 		windows.StringToUTF16Ptr(urlStr),
 | |
| 		getProxyForURLOpts(),
 | |
| 		&out,
 | |
| 	)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	defer out.free()
 | |
| 	return windows.UTF16PtrToString(out.Proxy), nil
 | |
| }
 | |
| 
 | |
| func sysAuthHeaderWindows(u *url.URL) (string, error) {
 | |
| 	spn := "HTTP/" + u.Hostname()
 | |
| 	creds, err := negotiate.AcquireCurrentUserCredentials()
 | |
| 	if err != nil {
 | |
| 		return "", fmt.Errorf("negotiate.AcquireCurrentUserCredentials: %w", err)
 | |
| 	}
 | |
| 	defer creds.Release()
 | |
| 
 | |
| 	secCtx, token, err := negotiate.NewClientContext(creds, spn)
 | |
| 	if err != nil {
 | |
| 		return "", fmt.Errorf("negotiate.NewClientContext: %w", err)
 | |
| 	}
 | |
| 	defer secCtx.Release()
 | |
| 
 | |
| 	return "Negotiate " + base64.StdEncoding.EncodeToString(token), nil
 | |
| }
 |