summaryrefslogtreecommitdiffhomepage
path: root/net/tshttpproxy
diff options
context:
space:
mode:
Diffstat (limited to 'net/tshttpproxy')
-rw-r--r--net/tshttpproxy/mksyscall.go22
-rw-r--r--net/tshttpproxy/tshttpproxy_linux.go48
-rw-r--r--net/tshttpproxy/tshttpproxy_synology_test.go752
-rw-r--r--net/tshttpproxy/tshttpproxy_windows.go552
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
+}