diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 8ab4467a0..6d0fc95cf 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -6,14 +6,20 @@ import ( "context" + "crypto/tls" "encoding/json" "errors" "flag" + "fmt" "log" + "net/http" + "net/http/httptrace" + "net/url" "os" "github.com/peterbourgon/ff/v2/ffcli" "tailscale.com/net/interfaces" + "tailscale.com/net/tshttpproxy" "tailscale.com/wgengine/monitor" ) @@ -22,13 +28,15 @@ Exec: runDebug, FlagSet: (func() *flag.FlagSet { fs := flag.NewFlagSet("debug", flag.ExitOnError) - fs.BoolVar(&debugArgs.monitor, "monitor", false, "") + fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.") + fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.") return fs })(), } var debugArgs struct { monitor bool + getURL string } func runDebug(ctx context.Context, args []string) error { @@ -38,6 +46,9 @@ func runDebug(ctx context.Context, args []string) error { if debugArgs.monitor { return runMonitor(ctx) } + if debugArgs.getURL != "" { + return getURL(ctx, debugArgs.getURL) + } return errors.New("only --monitor is available at the moment") } @@ -64,3 +75,46 @@ func runMonitor(ctx context.Context) error { log.Printf("Started link change monitor; waiting...") select {} } + +func getURL(ctx context.Context, urlStr string) error { + if urlStr == "login" { + urlStr = "https://login.tailscale.com" + } + log.SetOutput(os.Stdout) + ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ + GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) }, + GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) }, + DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) }, + DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) }, + TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") }, + TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) }, + WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) }, + }) + req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil) + if err != nil { + return fmt.Errorf("http.NewRequestWithContext: %v", err) + } + proxyURL, err := tshttpproxy.ProxyFromEnvironment(req) + if err != nil { + return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err) + } + log.Printf("proxy: %v", proxyURL) + tr := &http.Transport{ + Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil }, + ProxyConnectHeader: http.Header{}, + DisableKeepAlives: true, + } + if proxyURL != nil { + auth, err := tshttpproxy.GetAuthHeader(proxyURL) + log.Printf("tshttpproxy.GetAuthHeader(%v) = %q, %v", proxyURL, auth, err) + if err == nil && auth != "" { + tr.ProxyConnectHeader.Set("Authorization", auth) + } + } + res, err := tr.RoundTrip(req) + if err != nil { + return fmt.Errorf("Transport.RoundTrip: %v", err) + } + defer res.Body.Close() + return res.Write(os.Stdout) +} diff --git a/go.mod b/go.mod index 35b643ea8..3f0095e06 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module tailscale.com go 1.14 require ( + github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 github.com/coreos/go-iptables v0.4.5 diff --git a/go.sum b/go.sum index 76f3539fa..a0c665cac 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4= +github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4= diff --git a/net/tshttpproxy/tshttpproxy.go b/net/tshttpproxy/tshttpproxy.go index c857c2804..b14c4386d 100644 --- a/net/tshttpproxy/tshttpproxy.go +++ b/net/tshttpproxy/tshttpproxy.go @@ -31,3 +31,13 @@ func ProxyFromEnvironment(req *http.Request) (*url.URL, error) { return nil, err } + +var sysAuthHeader func(*url.URL) (string, error) + +// GetAuthHeader returns the Authorization header value to send to proxy u. +func GetAuthHeader(u *url.URL) (string, error) { + if sysAuthHeader != nil { + return sysAuthHeader(u) + } + return "", nil +} diff --git a/net/tshttpproxy/tshttpproxy_windows.go b/net/tshttpproxy/tshttpproxy_windows.go index cf67483f6..53df50ae6 100644 --- a/net/tshttpproxy/tshttpproxy_windows.go +++ b/net/tshttpproxy/tshttpproxy_windows.go @@ -5,6 +5,8 @@ package tshttpproxy import ( + "encoding/base64" + "fmt" "log" "net/http" "net/url" @@ -12,6 +14,7 @@ "syscall" "unsafe" + "github.com/alexbrainman/sspi/negotiate" "golang.org/x/sys/windows" ) @@ -24,6 +27,7 @@ func init() { sysProxyFromEnv = proxyFromWinHTTP + sysAuthHeader = sysAuthHeaderWindows } func proxyFromWinHTTP(req *http.Request) (*url.URL, error) { @@ -144,3 +148,20 @@ func (hi winHTTPInternet) GetProxyForURL(urlStr string) (string, error) { } return "", err } + +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 +}