diff options
Diffstat (limited to 'net/tshttpproxy')
| -rw-r--r-- | net/tshttpproxy/mksyscall.go | 22 | ||||
| -rw-r--r-- | net/tshttpproxy/tshttpproxy_linux.go | 48 | ||||
| -rw-r--r-- | net/tshttpproxy/tshttpproxy_synology_test.go | 752 | ||||
| -rw-r--r-- | net/tshttpproxy/tshttpproxy_windows.go | 552 |
4 files changed, 687 insertions, 687 deletions
diff --git a/net/tshttpproxy/mksyscall.go b/net/tshttpproxy/mksyscall.go index f8fdae89b..467dc4917 100644 --- a/net/tshttpproxy/mksyscall.go +++ b/net/tshttpproxy/mksyscall.go @@ -1,11 +1,11 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package tshttpproxy - -//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go - -//sys globalFree(hglobal winHGlobal) (err error) [failretval==0] = kernel32.GlobalFree -//sys winHTTPCloseHandle(whi winHTTPInternet) (err error) [failretval==0] = winhttp.WinHttpCloseHandle -//sys winHTTPGetProxyForURL(whi winHTTPInternet, url *uint16, options *winHTTPAutoProxyOptions, proxyInfo *winHTTPProxyInfo) (err error) [failretval==0] = winhttp.WinHttpGetProxyForUrl -//sys winHTTPOpen(agent *uint16, accessType uint32, proxy *uint16, proxyBypass *uint16, flags uint32) (whi winHTTPInternet, err error) [failretval==0] = winhttp.WinHttpOpen +// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package tshttpproxy
+
+//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
+
+//sys globalFree(hglobal winHGlobal) (err error) [failretval==0] = kernel32.GlobalFree
+//sys winHTTPCloseHandle(whi winHTTPInternet) (err error) [failretval==0] = winhttp.WinHttpCloseHandle
+//sys winHTTPGetProxyForURL(whi winHTTPInternet, url *uint16, options *winHTTPAutoProxyOptions, proxyInfo *winHTTPProxyInfo) (err error) [failretval==0] = winhttp.WinHttpGetProxyForUrl
+//sys winHTTPOpen(agent *uint16, accessType uint32, proxy *uint16, proxyBypass *uint16, flags uint32) (whi winHTTPInternet, err error) [failretval==0] = winhttp.WinHttpOpen
diff --git a/net/tshttpproxy/tshttpproxy_linux.go b/net/tshttpproxy/tshttpproxy_linux.go index b241c256d..09019893a 100644 --- a/net/tshttpproxy/tshttpproxy_linux.go +++ b/net/tshttpproxy/tshttpproxy_linux.go @@ -1,24 +1,24 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -//go:build linux - -package tshttpproxy - -import ( - "net/http" - "net/url" - - "tailscale.com/version/distro" -) - -func init() { - sysProxyFromEnv = linuxSysProxyFromEnv -} - -func linuxSysProxyFromEnv(req *http.Request) (*url.URL, error) { - if distro.Get() == distro.Synology { - return synologyProxyFromConfigCached(req) - } - return nil, nil -} +// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build linux
+
+package tshttpproxy
+
+import (
+ "net/http"
+ "net/url"
+
+ "tailscale.com/version/distro"
+)
+
+func init() {
+ sysProxyFromEnv = linuxSysProxyFromEnv
+}
+
+func linuxSysProxyFromEnv(req *http.Request) (*url.URL, error) {
+ if distro.Get() == distro.Synology {
+ return synologyProxyFromConfigCached(req)
+ }
+ return nil, nil
+}
diff --git a/net/tshttpproxy/tshttpproxy_synology_test.go b/net/tshttpproxy/tshttpproxy_synology_test.go index 3061740f3..e11c9d059 100644 --- a/net/tshttpproxy/tshttpproxy_synology_test.go +++ b/net/tshttpproxy/tshttpproxy_synology_test.go @@ -1,376 +1,376 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -//go:build linux - -package tshttpproxy - -import ( - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "tailscale.com/tstest" -) - -func TestSynologyProxyFromConfigCached(t *testing.T) { - req, err := http.NewRequest("GET", "http://example.org/", nil) - if err != nil { - t.Fatal(err) - } - - tstest.Replace(t, &synologyProxyConfigPath, filepath.Join(t.TempDir(), "proxy.conf")) - - t.Run("no config file", func(t *testing.T) { - if _, err := os.Stat(synologyProxyConfigPath); err == nil { - t.Fatalf("%s must not exist for this test", synologyProxyConfigPath) - } - - cache.updated = time.Time{} - cache.httpProxy = nil - cache.httpsProxy = nil - - if val, err := synologyProxyFromConfigCached(req); val != nil || err != nil { - t.Fatalf("got %s, %v; want nil, nil", val, err) - } - - if got, want := cache.updated, time.Unix(0, 0); got != want { - t.Fatalf("got %s, want %s", got, want) - } - if cache.httpProxy != nil { - t.Fatalf("got %s, want nil", cache.httpProxy) - } - if cache.httpsProxy != nil { - t.Fatalf("got %s, want nil", cache.httpsProxy) - } - }) - - t.Run("config file updated", func(t *testing.T) { - cache.updated = time.Now() - cache.httpProxy = nil - cache.httpsProxy = nil - - if err := os.WriteFile(synologyProxyConfigPath, []byte(` -proxy_enabled=yes -http_host=10.0.0.55 -http_port=80 -https_host=10.0.0.66 -https_port=443 - `), 0600); err != nil { - t.Fatal(err) - } - - val, err := synologyProxyFromConfigCached(req) - if err != nil { - t.Fatal(err) - } - - if cache.httpProxy == nil { - t.Fatal("http proxy was not cached") - } - if cache.httpsProxy == nil { - t.Fatal("https proxy was not cached") - } - - if want := urlMustParse("http://10.0.0.55:80"); val.String() != want.String() { - t.Fatalf("got %s; want %s", val, want) - } - }) - - t.Run("config file removed", func(t *testing.T) { - cache.updated = time.Now() - cache.httpProxy = urlMustParse("http://127.0.0.1/") - cache.httpsProxy = urlMustParse("http://127.0.0.1/") - - if err := os.Remove(synologyProxyConfigPath); err != nil && !os.IsNotExist(err) { - t.Fatal(err) - } - - val, err := synologyProxyFromConfigCached(req) - if err != nil { - t.Fatal(err) - } - if val != nil { - t.Fatalf("got %s; want nil", val) - } - if cache.httpProxy != nil { - t.Fatalf("got %s, want nil", cache.httpProxy) - } - if cache.httpsProxy != nil { - t.Fatalf("got %s, want nil", cache.httpsProxy) - } - }) - - t.Run("picks proxy from request scheme", func(t *testing.T) { - cache.updated = time.Now() - cache.httpProxy = nil - cache.httpsProxy = nil - - if err := os.WriteFile(synologyProxyConfigPath, []byte(` -proxy_enabled=yes -http_host=10.0.0.55 -http_port=80 -https_host=10.0.0.66 -https_port=443 - `), 0600); err != nil { - t.Fatal(err) - } - - httpReq, err := http.NewRequest("GET", "http://example.com", nil) - if err != nil { - t.Fatal(err) - } - val, err := synologyProxyFromConfigCached(httpReq) - if err != nil { - t.Fatal(err) - } - if val == nil { - t.Fatalf("got nil, want an http URL") - } - if got, want := val.String(), "http://10.0.0.55:80"; got != want { - t.Fatalf("got %q, want %q", got, want) - } - - httpsReq, err := http.NewRequest("GET", "https://example.com", nil) - if err != nil { - t.Fatal(err) - } - val, err = synologyProxyFromConfigCached(httpsReq) - if err != nil { - t.Fatal(err) - } - if val == nil { - t.Fatalf("got nil, want an http URL") - } - if got, want := val.String(), "http://10.0.0.66:443"; got != want { - t.Fatalf("got %q, want %q", got, want) - } - }) -} - -func TestSynologyProxiesFromConfig(t *testing.T) { - var ( - openReader io.ReadCloser - openErr error - ) - tstest.Replace(t, &openSynologyProxyConf, func() (io.ReadCloser, error) { - return openReader, openErr - }) - - t.Run("with config", func(t *testing.T) { - mc := &mustCloser{Reader: strings.NewReader(` -proxy_user=foo -proxy_pwd=bar -proxy_enabled=yes -adv_enabled=yes -bypass_enabled=yes -auth_enabled=yes -https_host=10.0.0.66 -https_port=8443 -http_host=10.0.0.55 -http_port=80 - `)} - defer mc.check(t) - openReader = mc - - httpProxy, httpsProxy, err := synologyProxiesFromConfig() - - if got, want := err, openErr; got != want { - t.Fatalf("got %s, want %s", got, want) - } - - if got, want := httpsProxy, urlMustParse("http://foo:bar@10.0.0.66:8443"); got.String() != want.String() { - t.Fatalf("got %s, want %s", got, want) - } - - if got, want := err, openErr; got != want { - t.Fatalf("got %s, want %s", got, want) - } - - if got, want := httpProxy, urlMustParse("http://foo:bar@10.0.0.55:80"); got.String() != want.String() { - t.Fatalf("got %s, want %s", got, want) - } - - }) - - t.Run("nonexistent config", func(t *testing.T) { - openReader = nil - openErr = os.ErrNotExist - - httpProxy, httpsProxy, err := synologyProxiesFromConfig() - if err != nil { - t.Fatalf("expected no error, got %s", err) - } - if httpProxy != nil { - t.Fatalf("expected no url, got %s", httpProxy) - } - if httpsProxy != nil { - t.Fatalf("expected no url, got %s", httpsProxy) - } - }) - - t.Run("error opening config", func(t *testing.T) { - openReader = nil - openErr = errors.New("example error") - - httpProxy, httpsProxy, err := synologyProxiesFromConfig() - if err != openErr { - t.Fatalf("expected %s, got %s", openErr, err) - } - if httpProxy != nil { - t.Fatalf("expected no url, got %s", httpProxy) - } - if httpsProxy != nil { - t.Fatalf("expected no url, got %s", httpsProxy) - } - }) - -} - -func TestParseSynologyConfig(t *testing.T) { - cases := map[string]struct { - input string - httpProxy *url.URL - httpsProxy *url.URL - err error - }{ - "populated": { - input: ` -proxy_user=foo -proxy_pwd=bar -proxy_enabled=yes -adv_enabled=yes -bypass_enabled=yes -auth_enabled=yes -https_host=10.0.0.66 -https_port=8443 -http_host=10.0.0.55 -http_port=80 -`, - httpProxy: urlMustParse("http://foo:bar@10.0.0.55:80"), - httpsProxy: urlMustParse("http://foo:bar@10.0.0.66:8443"), - err: nil, - }, - "no-auth": { - input: ` -proxy_user=foo -proxy_pwd=bar -proxy_enabled=yes -adv_enabled=yes -bypass_enabled=yes -auth_enabled=no -https_host=10.0.0.66 -https_port=8443 -http_host=10.0.0.55 -http_port=80 -`, - httpProxy: urlMustParse("http://10.0.0.55:80"), - httpsProxy: urlMustParse("http://10.0.0.66:8443"), - err: nil, - }, - "http-only": { - input: ` -proxy_user=foo -proxy_pwd=bar -proxy_enabled=yes -adv_enabled=yes -bypass_enabled=yes -auth_enabled=yes -https_host= -https_port=8443 -http_host=10.0.0.55 -http_port=80 -`, - httpProxy: urlMustParse("http://foo:bar@10.0.0.55:80"), - httpsProxy: nil, - err: nil, - }, - "empty": { - input: ` -proxy_user= -proxy_pwd= -proxy_enabled= -adv_enabled= -bypass_enabled= -auth_enabled= -https_host= -https_port= -http_host= -http_port= -`, - httpProxy: nil, - httpsProxy: nil, - err: nil, - }, - } - - for name, example := range cases { - t.Run(name, func(t *testing.T) { - httpProxy, httpsProxy, err := parseSynologyConfig(strings.NewReader(example.input)) - if err != example.err { - t.Fatal(err) - } - if example.err != nil { - return - } - - if example.httpProxy == nil && httpProxy != nil { - t.Fatalf("got %s, want nil", httpProxy) - } - - if example.httpProxy != nil { - if httpProxy == nil { - t.Fatalf("got nil, want %s", example.httpProxy) - } - - if got, want := example.httpProxy.String(), httpProxy.String(); got != want { - t.Fatalf("got %s, want %s", got, want) - } - } - - if example.httpsProxy == nil && httpsProxy != nil { - t.Fatalf("got %s, want nil", httpProxy) - } - - if example.httpsProxy != nil { - if httpsProxy == nil { - t.Fatalf("got nil, want %s", example.httpsProxy) - } - - if got, want := example.httpsProxy.String(), httpsProxy.String(); got != want { - t.Fatalf("got %s, want %s", got, want) - } - } - }) - } -} -func urlMustParse(u string) *url.URL { - r, err := url.Parse(u) - if err != nil { - panic(fmt.Sprintf("urlMustParse: %s", err)) - } - return r -} - -type mustCloser struct { - io.Reader - closed bool -} - -func (m *mustCloser) Close() error { - m.closed = true - return nil -} - -func (m *mustCloser) check(t *testing.T) { - if !m.closed { - t.Errorf("mustCloser wrapping %#v was not closed at time of check", m.Reader) - } -} +// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build linux
+
+package tshttpproxy
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "tailscale.com/tstest"
+)
+
+func TestSynologyProxyFromConfigCached(t *testing.T) {
+ req, err := http.NewRequest("GET", "http://example.org/", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tstest.Replace(t, &synologyProxyConfigPath, filepath.Join(t.TempDir(), "proxy.conf"))
+
+ t.Run("no config file", func(t *testing.T) {
+ if _, err := os.Stat(synologyProxyConfigPath); err == nil {
+ t.Fatalf("%s must not exist for this test", synologyProxyConfigPath)
+ }
+
+ cache.updated = time.Time{}
+ cache.httpProxy = nil
+ cache.httpsProxy = nil
+
+ if val, err := synologyProxyFromConfigCached(req); val != nil || err != nil {
+ t.Fatalf("got %s, %v; want nil, nil", val, err)
+ }
+
+ if got, want := cache.updated, time.Unix(0, 0); got != want {
+ t.Fatalf("got %s, want %s", got, want)
+ }
+ if cache.httpProxy != nil {
+ t.Fatalf("got %s, want nil", cache.httpProxy)
+ }
+ if cache.httpsProxy != nil {
+ t.Fatalf("got %s, want nil", cache.httpsProxy)
+ }
+ })
+
+ t.Run("config file updated", func(t *testing.T) {
+ cache.updated = time.Now()
+ cache.httpProxy = nil
+ cache.httpsProxy = nil
+
+ if err := os.WriteFile(synologyProxyConfigPath, []byte(`
+proxy_enabled=yes
+http_host=10.0.0.55
+http_port=80
+https_host=10.0.0.66
+https_port=443
+ `), 0600); err != nil {
+ t.Fatal(err)
+ }
+
+ val, err := synologyProxyFromConfigCached(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if cache.httpProxy == nil {
+ t.Fatal("http proxy was not cached")
+ }
+ if cache.httpsProxy == nil {
+ t.Fatal("https proxy was not cached")
+ }
+
+ if want := urlMustParse("http://10.0.0.55:80"); val.String() != want.String() {
+ t.Fatalf("got %s; want %s", val, want)
+ }
+ })
+
+ t.Run("config file removed", func(t *testing.T) {
+ cache.updated = time.Now()
+ cache.httpProxy = urlMustParse("http://127.0.0.1/")
+ cache.httpsProxy = urlMustParse("http://127.0.0.1/")
+
+ if err := os.Remove(synologyProxyConfigPath); err != nil && !os.IsNotExist(err) {
+ t.Fatal(err)
+ }
+
+ val, err := synologyProxyFromConfigCached(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if val != nil {
+ t.Fatalf("got %s; want nil", val)
+ }
+ if cache.httpProxy != nil {
+ t.Fatalf("got %s, want nil", cache.httpProxy)
+ }
+ if cache.httpsProxy != nil {
+ t.Fatalf("got %s, want nil", cache.httpsProxy)
+ }
+ })
+
+ t.Run("picks proxy from request scheme", func(t *testing.T) {
+ cache.updated = time.Now()
+ cache.httpProxy = nil
+ cache.httpsProxy = nil
+
+ if err := os.WriteFile(synologyProxyConfigPath, []byte(`
+proxy_enabled=yes
+http_host=10.0.0.55
+http_port=80
+https_host=10.0.0.66
+https_port=443
+ `), 0600); err != nil {
+ t.Fatal(err)
+ }
+
+ httpReq, err := http.NewRequest("GET", "http://example.com", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ val, err := synologyProxyFromConfigCached(httpReq)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if val == nil {
+ t.Fatalf("got nil, want an http URL")
+ }
+ if got, want := val.String(), "http://10.0.0.55:80"; got != want {
+ t.Fatalf("got %q, want %q", got, want)
+ }
+
+ httpsReq, err := http.NewRequest("GET", "https://example.com", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ val, err = synologyProxyFromConfigCached(httpsReq)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if val == nil {
+ t.Fatalf("got nil, want an http URL")
+ }
+ if got, want := val.String(), "http://10.0.0.66:443"; got != want {
+ t.Fatalf("got %q, want %q", got, want)
+ }
+ })
+}
+
+func TestSynologyProxiesFromConfig(t *testing.T) {
+ var (
+ openReader io.ReadCloser
+ openErr error
+ )
+ tstest.Replace(t, &openSynologyProxyConf, func() (io.ReadCloser, error) {
+ return openReader, openErr
+ })
+
+ t.Run("with config", func(t *testing.T) {
+ mc := &mustCloser{Reader: strings.NewReader(`
+proxy_user=foo
+proxy_pwd=bar
+proxy_enabled=yes
+adv_enabled=yes
+bypass_enabled=yes
+auth_enabled=yes
+https_host=10.0.0.66
+https_port=8443
+http_host=10.0.0.55
+http_port=80
+ `)}
+ defer mc.check(t)
+ openReader = mc
+
+ httpProxy, httpsProxy, err := synologyProxiesFromConfig()
+
+ if got, want := err, openErr; got != want {
+ t.Fatalf("got %s, want %s", got, want)
+ }
+
+ if got, want := httpsProxy, urlMustParse("http://foo:bar@10.0.0.66:8443"); got.String() != want.String() {
+ t.Fatalf("got %s, want %s", got, want)
+ }
+
+ if got, want := err, openErr; got != want {
+ t.Fatalf("got %s, want %s", got, want)
+ }
+
+ if got, want := httpProxy, urlMustParse("http://foo:bar@10.0.0.55:80"); got.String() != want.String() {
+ t.Fatalf("got %s, want %s", got, want)
+ }
+
+ })
+
+ t.Run("nonexistent config", func(t *testing.T) {
+ openReader = nil
+ openErr = os.ErrNotExist
+
+ httpProxy, httpsProxy, err := synologyProxiesFromConfig()
+ if err != nil {
+ t.Fatalf("expected no error, got %s", err)
+ }
+ if httpProxy != nil {
+ t.Fatalf("expected no url, got %s", httpProxy)
+ }
+ if httpsProxy != nil {
+ t.Fatalf("expected no url, got %s", httpsProxy)
+ }
+ })
+
+ t.Run("error opening config", func(t *testing.T) {
+ openReader = nil
+ openErr = errors.New("example error")
+
+ httpProxy, httpsProxy, err := synologyProxiesFromConfig()
+ if err != openErr {
+ t.Fatalf("expected %s, got %s", openErr, err)
+ }
+ if httpProxy != nil {
+ t.Fatalf("expected no url, got %s", httpProxy)
+ }
+ if httpsProxy != nil {
+ t.Fatalf("expected no url, got %s", httpsProxy)
+ }
+ })
+
+}
+
+func TestParseSynologyConfig(t *testing.T) {
+ cases := map[string]struct {
+ input string
+ httpProxy *url.URL
+ httpsProxy *url.URL
+ err error
+ }{
+ "populated": {
+ input: `
+proxy_user=foo
+proxy_pwd=bar
+proxy_enabled=yes
+adv_enabled=yes
+bypass_enabled=yes
+auth_enabled=yes
+https_host=10.0.0.66
+https_port=8443
+http_host=10.0.0.55
+http_port=80
+`,
+ httpProxy: urlMustParse("http://foo:bar@10.0.0.55:80"),
+ httpsProxy: urlMustParse("http://foo:bar@10.0.0.66:8443"),
+ err: nil,
+ },
+ "no-auth": {
+ input: `
+proxy_user=foo
+proxy_pwd=bar
+proxy_enabled=yes
+adv_enabled=yes
+bypass_enabled=yes
+auth_enabled=no
+https_host=10.0.0.66
+https_port=8443
+http_host=10.0.0.55
+http_port=80
+`,
+ httpProxy: urlMustParse("http://10.0.0.55:80"),
+ httpsProxy: urlMustParse("http://10.0.0.66:8443"),
+ err: nil,
+ },
+ "http-only": {
+ input: `
+proxy_user=foo
+proxy_pwd=bar
+proxy_enabled=yes
+adv_enabled=yes
+bypass_enabled=yes
+auth_enabled=yes
+https_host=
+https_port=8443
+http_host=10.0.0.55
+http_port=80
+`,
+ httpProxy: urlMustParse("http://foo:bar@10.0.0.55:80"),
+ httpsProxy: nil,
+ err: nil,
+ },
+ "empty": {
+ input: `
+proxy_user=
+proxy_pwd=
+proxy_enabled=
+adv_enabled=
+bypass_enabled=
+auth_enabled=
+https_host=
+https_port=
+http_host=
+http_port=
+`,
+ httpProxy: nil,
+ httpsProxy: nil,
+ err: nil,
+ },
+ }
+
+ for name, example := range cases {
+ t.Run(name, func(t *testing.T) {
+ httpProxy, httpsProxy, err := parseSynologyConfig(strings.NewReader(example.input))
+ if err != example.err {
+ t.Fatal(err)
+ }
+ if example.err != nil {
+ return
+ }
+
+ if example.httpProxy == nil && httpProxy != nil {
+ t.Fatalf("got %s, want nil", httpProxy)
+ }
+
+ if example.httpProxy != nil {
+ if httpProxy == nil {
+ t.Fatalf("got nil, want %s", example.httpProxy)
+ }
+
+ if got, want := example.httpProxy.String(), httpProxy.String(); got != want {
+ t.Fatalf("got %s, want %s", got, want)
+ }
+ }
+
+ if example.httpsProxy == nil && httpsProxy != nil {
+ t.Fatalf("got %s, want nil", httpProxy)
+ }
+
+ if example.httpsProxy != nil {
+ if httpsProxy == nil {
+ t.Fatalf("got nil, want %s", example.httpsProxy)
+ }
+
+ if got, want := example.httpsProxy.String(), httpsProxy.String(); got != want {
+ t.Fatalf("got %s, want %s", got, want)
+ }
+ }
+ })
+ }
+}
+func urlMustParse(u string) *url.URL {
+ r, err := url.Parse(u)
+ if err != nil {
+ panic(fmt.Sprintf("urlMustParse: %s", err))
+ }
+ return r
+}
+
+type mustCloser struct {
+ io.Reader
+ closed bool
+}
+
+func (m *mustCloser) Close() error {
+ m.closed = true
+ return nil
+}
+
+func (m *mustCloser) check(t *testing.T) {
+ if !m.closed {
+ t.Errorf("mustCloser wrapping %#v was not closed at time of check", m.Reader)
+ }
+}
diff --git a/net/tshttpproxy/tshttpproxy_windows.go b/net/tshttpproxy/tshttpproxy_windows.go index 06a1f5ae4..cb6b24c83 100644 --- a/net/tshttpproxy/tshttpproxy_windows.go +++ b/net/tshttpproxy/tshttpproxy_windows.go @@ -1,276 +1,276 @@ -// 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 -} +// 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
+}
|
