summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--control/controlclient/direct.go2
-rw-r--r--derp/derphttp/derphttp_client.go75
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--logpolicy/logpolicy.go3
-rw-r--r--net/interfaces/interfaces.go18
-rw-r--r--net/tshttpproxy/tshttpproxy.go33
-rw-r--r--net/tshttpproxy/tshttpproxy_windows.go142
8 files changed, 276 insertions, 1 deletions
diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go
index a5b5ca203..8a0509cbf 100644
--- a/control/controlclient/direct.go
+++ b/control/controlclient/direct.go
@@ -35,6 +35,7 @@ import (
"tailscale.com/log/logheap"
"tailscale.com/net/netns"
"tailscale.com/net/tlsdial"
+ "tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/structs"
@@ -145,6 +146,7 @@ func NewDirect(opts Options) (*Direct, error) {
if httpc == nil {
dialer := netns.NewDialer()
tr := http.DefaultTransport.(*http.Transport).Clone()
+ tr.Proxy = tshttpproxy.ProxyFromEnvironment
tr.DialContext = dialer.DialContext
tr.ForceAttemptHTTP2 = true
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
diff --git a/derp/derphttp/derphttp_client.go b/derp/derphttp/derphttp_client.go
index 83697c257..16f56e9b6 100644
--- a/derp/derphttp/derphttp_client.go
+++ b/derp/derphttp/derphttp_client.go
@@ -29,6 +29,7 @@ import (
"tailscale.com/net/dnscache"
"tailscale.com/net/netns"
"tailscale.com/net/tlsdial"
+ "tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -420,6 +421,19 @@ const dialNodeTimeout = 1500 * time.Millisecond
// TODO(bradfitz): longer if no options remain perhaps? ... Or longer
// overall but have dialRegion start overlapping races?
func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, error) {
+ // First see if we need to use an HTTP proxy.
+ proxyReq := &http.Request{
+ Method: "GET", // doesn't really matter
+ URL: &url.URL{
+ Scheme: "https",
+ Host: c.tlsServerName(n),
+ Path: "/", // unused
+ },
+ }
+ if proxyURL, err := tshttpproxy.ProxyFromEnvironment(proxyReq); err == nil && proxyURL != nil {
+ return c.dialNodeUsingProxy(ctx, n, proxyURL)
+ }
+
type res struct {
c net.Conn
err error
@@ -480,6 +494,67 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
}
}
+func firstStr(a, b string) string {
+ if a != "" {
+ return a
+ }
+ return b
+}
+
+// dialNodeUsingProxy connects to n using a CONNECT to the HTTP(s) proxy in proxyURL.
+func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, proxyURL *url.URL) (proxyConn net.Conn, err error) {
+ pu := proxyURL
+ if pu.Scheme == "https" {
+ var d tls.Dialer
+ proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "443")))
+ } else {
+ var d net.Dialer
+ proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "80")))
+ }
+ defer func() {
+ if err != nil && proxyConn != nil {
+ // In a goroutine in case it's a *tls.Conn (that can block on Close)
+ // TODO(bradfitz): track the underlying tcp.Conn and just close that instead.
+ go proxyConn.Close()
+ }
+ }()
+ if err != nil {
+ return nil, err
+ }
+
+ done := make(chan struct{})
+ defer close(done)
+ go func() {
+ select {
+ case <-done:
+ return
+ case <-ctx.Done():
+ proxyConn.Close()
+ }
+ }()
+
+ target := net.JoinHostPort(n.HostName, "443")
+ if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", target, pu.Hostname()); err != nil {
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
+ return nil, err
+ }
+
+ br := bufio.NewReader(proxyConn)
+ res, err := http.ReadResponse(br, nil)
+ if err != nil {
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
+ return nil, err
+ }
+ if res.StatusCode != 200 {
+ return nil, fmt.Errorf("invalid response status from HTTP proxy %s on CONNECT to %s: %v", pu, target, res.Status)
+ }
+ return proxyConn, nil
+}
+
func (c *Client) Send(dstKey key.Public, b []byte) error {
client, _, err := c.connect(context.TODO(), "derphttp.Client.Send")
if err != nil {
diff --git a/go.mod b/go.mod
index a833cc7c1..35b643ea8 100644
--- a/go.mod
+++ b/go.mod
@@ -29,7 +29,7 @@ require (
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
- golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3
+ golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425
honnef.co/go/tools v0.0.1-2020.1.4
diff --git a/go.sum b/go.sum
index 91b9d592a..76f3539fa 100644
--- a/go.sum
+++ b/go.sum
@@ -141,6 +141,8 @@ golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d h1:QQrM/CCYEzTs91GZylDCQjGHudbPTxF/1fvXdVh5lMo=
+golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go
index 41b1c9189..eca6262c7 100644
--- a/logpolicy/logpolicy.go
+++ b/logpolicy/logpolicy.go
@@ -31,6 +31,7 @@ import (
"tailscale.com/logtail/filch"
"tailscale.com/net/netns"
"tailscale.com/net/tlsdial"
+ "tailscale.com/net/tshttpproxy"
"tailscale.com/paths"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
@@ -431,6 +432,8 @@ func newLogtailTransport(host string) *http.Transport {
// Start with a copy of http.DefaultTransport and tweak it a bit.
tr := http.DefaultTransport.(*http.Transport).Clone()
+ tr.Proxy = tshttpproxy.ProxyFromEnvironment
+
// We do our own zstd compression on uploads, and responses never contain any payload,
// so don't send "Accept-Encoding: gzip" to save a few bytes on the wire, since there
// will never be any body to decompress:
diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go
index 2d71d4d4e..3ef95c657 100644
--- a/net/interfaces/interfaces.go
+++ b/net/interfaces/interfaces.go
@@ -8,13 +8,19 @@ package interfaces
import (
"fmt"
"net"
+ "net/http"
"reflect"
"strings"
"inet.af/netaddr"
"tailscale.com/net/tsaddr"
+ "tailscale.com/net/tshttpproxy"
)
+// LoginEndpointForProxyDetermination is the URL used for testing
+// which HTTP proxy the system should use.
+var LoginEndpointForProxyDetermination = "https://login.tailscale.com/"
+
// Tailscale returns the current machine's Tailscale interface, if any.
// If none is found, all zero values are returned.
// A non-nil error is only returned on a problem listing the system interfaces.
@@ -168,6 +174,9 @@ type State struct {
// DefaultRouteInterface is the interface name for the machine's default route.
// It is not yet populated on all OSes.
DefaultRouteInterface string
+
+ // HTTPProxy is the HTTP proxy to use.
+ HTTPProxy string
}
func (s *State) Equal(s2 *State) bool {
@@ -205,6 +214,15 @@ func GetState() (*State, error) {
return nil, err
}
s.DefaultRouteInterface, _ = DefaultRouteInterface()
+
+ req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
+ if err != nil {
+ return nil, err
+ }
+ if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
+ s.HTTPProxy = u.String()
+ }
+
return s, nil
}
diff --git a/net/tshttpproxy/tshttpproxy.go b/net/tshttpproxy/tshttpproxy.go
new file mode 100644
index 000000000..c857c2804
--- /dev/null
+++ b/net/tshttpproxy/tshttpproxy.go
@@ -0,0 +1,33 @@
+// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package tshttpproxy contains Tailscale additions to httpproxy not available
+// in golang.org/x/net/http/httpproxy. Notably, it aims to support Windows better.
+package tshttpproxy
+
+import (
+ "net/http"
+ "net/url"
+)
+
+// sysProxyFromEnv, if non-nil, specifies a platform-specific ProxyFromEnvironment
+// func to use if http.ProxyFromEnvironment doesn't return a proxy.
+// For example, WPAD PAC files on Windows.
+var sysProxyFromEnv func(*http.Request) (*url.URL, error)
+
+func ProxyFromEnvironment(req *http.Request) (*url.URL, error) {
+ u, err := http.ProxyFromEnvironment(req)
+ if u != nil && err == nil {
+ return u, nil
+ }
+
+ if sysProxyFromEnv != nil {
+ u, err := sysProxyFromEnv(req)
+ if u != nil && err == nil {
+ return u, nil
+ }
+ }
+
+ return nil, err
+}
diff --git a/net/tshttpproxy/tshttpproxy_windows.go b/net/tshttpproxy/tshttpproxy_windows.go
new file mode 100644
index 000000000..382df745f
--- /dev/null
+++ b/net/tshttpproxy/tshttpproxy_windows.go
@@ -0,0 +1,142 @@
+// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tshttpproxy
+
+import (
+ "log"
+ "net/http"
+ "net/url"
+ "strings"
+ "syscall"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+)
+
+var (
+ winHTTP = windows.NewLazySystemDLL("winhttp.dll")
+ httpOpenProc = winHTTP.NewProc("WinHttpOpen")
+ closeHandleProc = winHTTP.NewProc("WinHttpCloseHandle")
+ getProxyForUrlProc = winHTTP.NewProc("WinHttpGetProxyForUrl")
+)
+
+func init() {
+ sysProxyFromEnv = proxyFromWinHTTP
+}
+
+func proxyFromWinHTTP(req *http.Request) (*url.URL, error) {
+ if req.URL == nil {
+ return nil, nil
+ }
+ urlStr := req.URL.String()
+
+ whi, err := winHTTPOpen()
+ if err != nil {
+ // Log but otherwise ignore the error.
+ log.Printf("winhttp: Open: %v", err)
+ return nil, nil
+ }
+ defer whi.Close()
+
+ v, err := whi.GetProxyForURL(urlStr)
+ if err != nil {
+ // See https://docs.microsoft.com/en-us/windows/win32/winhttp/error-messages
+ const ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
+ if err == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
+ return nil, nil
+ }
+ log.Printf("winhttp: GetProxyForURL(%q): %v (%T, %#v)", urlStr, err, err, err)
+ return nil, nil
+ }
+ if v != "" {
+ if !strings.HasPrefix(v, "https://") {
+ v = "http://" + v
+ }
+ if u, err := url.Parse(v); err == nil {
+ return u, nil
+ }
+ }
+
+ return nil, nil
+}
+
+var userAgent = windows.StringToUTF16Ptr("Tailscale")
+
+const (
+ 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
+)
+
+func winHTTPOpen() (winHTTPInternet, error) {
+ if err := httpOpenProc.Find(); err != nil {
+ return 0, err
+ }
+ r, _, err := httpOpenProc.Call(
+ uintptr(unsafe.Pointer(userAgent)),
+ winHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
+ 0, /* WINHTTP_NO_PROXY_NAME */
+ 0, /* WINHTTP_NO_PROXY_BYPASS */
+ 0)
+ if r == 0 {
+ return 0, err
+ }
+ return winHTTPInternet(r), nil
+}
+
+type winHTTPInternet windows.Handle
+
+func (hi winHTTPInternet) Close() error {
+ if err := closeHandleProc.Find(); err != nil {
+ return err
+ }
+ r, _, err := closeHandleProc.Call(uintptr(hi))
+ if r == 1 {
+ return nil
+ }
+ return err
+}
+
+// WINHTTP_AUTOPROXY_OPTIONS
+// https://docs.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_autoproxy_options
+type autoProxyOptions struct {
+ DwFlags uint32
+ DwAutoDetectFlags uint32
+ AutoConfigUrl *uint16
+ _ uintptr
+ _ uint32
+ FAutoLogonIfChallenged bool
+}
+
+// WINHTTP_PROXY_INFO
+// https://docs.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_proxy_info
+type winHTTPProxyInfo struct {
+ AccessType uint16
+ Proxy *uint16
+ ProxyBypass *uint16
+}
+
+var proxyForURLOpts = &autoProxyOptions{
+ 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) {
+ if err := getProxyForUrlProc.Find(); err != nil {
+ return "", err
+ }
+ var out winHTTPProxyInfo
+ r, _, err := getProxyForUrlProc.Call(
+ uintptr(hi),
+ uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(urlStr))),
+ uintptr(unsafe.Pointer(proxyForURLOpts)),
+ uintptr(unsafe.Pointer(&out)))
+ if r == 1 {
+ return windows.UTF16PtrToString(out.Proxy), nil
+ }
+ return "", err
+}