| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- // 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"
- "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)
- // Seen on Windows 8.1. (https://github.com/tailscale/tailscale/issues/879)
- // TODO(bradfitz): figure this out.
- setNoProxyUntil(time.Hour)
- 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 proxyForURLOpts = &winHTTPAutoProxyOptions{
- DwFlags: winHTTP_AUTOPROXY_ALLOW_AUTOCONFIG | winHTTP_AUTOPROXY_AUTO_DETECT,
- DwAutoDetectFlags: winHTTP_AUTO_DETECT_TYPE_DHCP, // | winHTTP_AUTO_DETECT_TYPE_DNS_A,
- }
- func (hi winHTTPInternet) GetProxyForURL(urlStr string) (string, error) {
- var out winHTTPProxyInfo
- err := winHTTPGetProxyForURL(
- hi,
- windows.StringToUTF16Ptr(urlStr),
- proxyForURLOpts,
- &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
- }
|