tshttpproxy_windows.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package tshttpproxy
  4. import (
  5. "context"
  6. "encoding/base64"
  7. "fmt"
  8. "log"
  9. "net/http"
  10. "net/url"
  11. "runtime"
  12. "strings"
  13. "sync"
  14. "syscall"
  15. "time"
  16. "unsafe"
  17. "github.com/alexbrainman/sspi/negotiate"
  18. "golang.org/x/sys/windows"
  19. "tailscale.com/hostinfo"
  20. "tailscale.com/syncs"
  21. "tailscale.com/types/logger"
  22. "tailscale.com/util/clientmetric"
  23. "tailscale.com/util/cmpver"
  24. )
  25. func init() {
  26. sysProxyFromEnv = proxyFromWinHTTPOrCache
  27. sysAuthHeader = sysAuthHeaderWindows
  28. }
  29. var cachedProxy struct {
  30. sync.Mutex
  31. val *url.URL
  32. }
  33. // proxyErrorf is a rate-limited logger specifically for errors asking
  34. // WinHTTP for the proxy information. We don't want to log about
  35. // errors often, otherwise the log message itself will generate a new
  36. // HTTP request which ultimately will call back into us to log again,
  37. // forever. So for errors, we only log a bit.
  38. var proxyErrorf = logger.RateLimitedFn(log.Printf, 10*time.Minute, 2 /* burst*/, 10 /* maxCache */)
  39. var (
  40. metricSuccess = clientmetric.NewCounter("winhttp_proxy_success")
  41. metricErrDetectionFailed = clientmetric.NewCounter("winhttp_proxy_err_detection_failed")
  42. metricErrInvalidParameters = clientmetric.NewCounter("winhttp_proxy_err_invalid_param")
  43. metricErrDownloadScript = clientmetric.NewCounter("winhttp_proxy_err_download_script")
  44. metricErrTimeout = clientmetric.NewCounter("winhttp_proxy_err_timeout")
  45. metricErrOther = clientmetric.NewCounter("winhttp_proxy_err_other")
  46. )
  47. func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
  48. if req.URL == nil {
  49. return nil, nil
  50. }
  51. urlStr := req.URL.String()
  52. ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)
  53. defer cancel()
  54. type result struct {
  55. proxy *url.URL
  56. err error
  57. }
  58. resc := make(chan result, 1)
  59. go func() {
  60. proxy, err := proxyFromWinHTTP(ctx, urlStr)
  61. resc <- result{proxy, err}
  62. }()
  63. select {
  64. case res := <-resc:
  65. err := res.err
  66. if err == nil {
  67. metricSuccess.Add(1)
  68. cachedProxy.Lock()
  69. defer cachedProxy.Unlock()
  70. if was, now := fmt.Sprint(cachedProxy.val), fmt.Sprint(res.proxy); was != now {
  71. log.Printf("tshttpproxy: winhttp: updating cached proxy setting from %v to %v", was, now)
  72. }
  73. cachedProxy.val = res.proxy
  74. return res.proxy, nil
  75. }
  76. // See https://docs.microsoft.com/en-us/windows/win32/winhttp/error-messages
  77. const (
  78. ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
  79. ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT = 12167
  80. )
  81. if err == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
  82. metricErrDetectionFailed.Add(1)
  83. setNoProxyUntil(10 * time.Second)
  84. return nil, nil
  85. }
  86. if err == windows.ERROR_INVALID_PARAMETER {
  87. metricErrInvalidParameters.Add(1)
  88. // Seen on Windows 8.1. (https://github.com/tailscale/tailscale/issues/879)
  89. // TODO(bradfitz): figure this out.
  90. setNoProxyUntil(time.Hour)
  91. proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): ERROR_INVALID_PARAMETER [unexpected]", urlStr)
  92. return nil, nil
  93. }
  94. proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): %v/%#v", urlStr, err, err)
  95. if err == syscall.Errno(ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT) {
  96. metricErrDownloadScript.Add(1)
  97. setNoProxyUntil(10 * time.Second)
  98. return nil, nil
  99. }
  100. metricErrOther.Add(1)
  101. return nil, err
  102. case <-ctx.Done():
  103. metricErrTimeout.Add(1)
  104. cachedProxy.Lock()
  105. defer cachedProxy.Unlock()
  106. proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): timeout; using cached proxy %v", urlStr, cachedProxy.val)
  107. return cachedProxy.val, nil
  108. }
  109. }
  110. func proxyFromWinHTTP(ctx context.Context, urlStr string) (proxy *url.URL, err error) {
  111. runtime.LockOSThread()
  112. defer runtime.UnlockOSThread()
  113. whi, err := httpOpen()
  114. if err != nil {
  115. proxyErrorf("winhttp: Open: %v", err)
  116. return nil, err
  117. }
  118. defer whi.Close()
  119. t0 := time.Now()
  120. v, err := whi.GetProxyForURL(urlStr)
  121. td := time.Since(t0).Round(time.Millisecond)
  122. if err := ctx.Err(); err != nil {
  123. log.Printf("tshttpproxy: winhttp: context canceled, ignoring GetProxyForURL(%q) after %v", urlStr, td)
  124. return nil, err
  125. }
  126. if err != nil {
  127. return nil, err
  128. }
  129. if v == "" {
  130. return nil, nil
  131. }
  132. // Discard all but first proxy value for now.
  133. if i := strings.Index(v, ";"); i != -1 {
  134. v = v[:i]
  135. }
  136. if !strings.HasPrefix(v, "https://") {
  137. v = "http://" + v
  138. }
  139. return url.Parse(v)
  140. }
  141. var userAgent = windows.StringToUTF16Ptr("Tailscale")
  142. const (
  143. winHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0
  144. winHTTP_ACCESS_TYPE_AUTOMATIC_PROXY = 4
  145. winHTTP_AUTOPROXY_ALLOW_AUTOCONFIG = 0x00000100
  146. winHTTP_AUTOPROXY_AUTO_DETECT = 1
  147. winHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
  148. winHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002
  149. )
  150. // Windows 8.1 is actually Windows 6.3 under the hood. Yay, marketing!
  151. const win8dot1Ver = "6.3"
  152. // accessType is the flag we must pass to WinHttpOpen for proxy resolution
  153. // depending on whether or not we're running Windows < 8.1
  154. var accessType syncs.AtomicValue[uint32]
  155. func getAccessFlag() uint32 {
  156. if flag, ok := accessType.LoadOk(); ok {
  157. return flag
  158. }
  159. var flag uint32
  160. if cmpver.Compare(hostinfo.GetOSVersion(), win8dot1Ver) < 0 {
  161. flag = winHTTP_ACCESS_TYPE_DEFAULT_PROXY
  162. } else {
  163. flag = winHTTP_ACCESS_TYPE_AUTOMATIC_PROXY
  164. }
  165. accessType.Store(flag)
  166. return flag
  167. }
  168. func httpOpen() (winHTTPInternet, error) {
  169. return winHTTPOpen(
  170. userAgent,
  171. getAccessFlag(),
  172. nil, /* WINHTTP_NO_PROXY_NAME */
  173. nil, /* WINHTTP_NO_PROXY_BYPASS */
  174. 0,
  175. )
  176. }
  177. type winHTTPInternet windows.Handle
  178. func (hi winHTTPInternet) Close() error {
  179. return winHTTPCloseHandle(hi)
  180. }
  181. // WINHTTP_AUTOPROXY_OPTIONS
  182. // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_autoproxy_options
  183. type winHTTPAutoProxyOptions struct {
  184. DwFlags uint32
  185. DwAutoDetectFlags uint32
  186. AutoConfigUrl *uint16
  187. _ uintptr
  188. _ uint32
  189. FAutoLogonIfChallenged int32 // BOOL
  190. }
  191. // WINHTTP_PROXY_INFO
  192. // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_proxy_info
  193. type winHTTPProxyInfo struct {
  194. AccessType uint32
  195. Proxy *uint16
  196. ProxyBypass *uint16
  197. }
  198. type winHGlobal windows.Handle
  199. func globalFreeUTF16Ptr(p *uint16) error {
  200. return globalFree((winHGlobal)(unsafe.Pointer(p)))
  201. }
  202. func (pi *winHTTPProxyInfo) free() {
  203. if pi.Proxy != nil {
  204. globalFreeUTF16Ptr(pi.Proxy)
  205. pi.Proxy = nil
  206. }
  207. if pi.ProxyBypass != nil {
  208. globalFreeUTF16Ptr(pi.ProxyBypass)
  209. pi.ProxyBypass = nil
  210. }
  211. }
  212. var proxyForURLOpts = &winHTTPAutoProxyOptions{
  213. DwFlags: winHTTP_AUTOPROXY_ALLOW_AUTOCONFIG | winHTTP_AUTOPROXY_AUTO_DETECT,
  214. DwAutoDetectFlags: winHTTP_AUTO_DETECT_TYPE_DHCP, // | winHTTP_AUTO_DETECT_TYPE_DNS_A,
  215. }
  216. func (hi winHTTPInternet) GetProxyForURL(urlStr string) (string, error) {
  217. var out winHTTPProxyInfo
  218. err := winHTTPGetProxyForURL(
  219. hi,
  220. windows.StringToUTF16Ptr(urlStr),
  221. proxyForURLOpts,
  222. &out,
  223. )
  224. if err != nil {
  225. return "", err
  226. }
  227. defer out.free()
  228. return windows.UTF16PtrToString(out.Proxy), nil
  229. }
  230. func sysAuthHeaderWindows(u *url.URL) (string, error) {
  231. spn := "HTTP/" + u.Hostname()
  232. creds, err := negotiate.AcquireCurrentUserCredentials()
  233. if err != nil {
  234. return "", fmt.Errorf("negotiate.AcquireCurrentUserCredentials: %w", err)
  235. }
  236. defer creds.Release()
  237. secCtx, token, err := negotiate.NewClientContext(creds, spn)
  238. if err != nil {
  239. return "", fmt.Errorf("negotiate.NewClientContext: %w", err)
  240. }
  241. defer secCtx.Release()
  242. return "Negotiate " + base64.StdEncoding.EncodeToString(token), nil
  243. }