diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index d30960342..c5d4ed01f 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -183,7 +183,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/kube from tailscale.com/ipn tailscale.com/log/filelogger from tailscale.com/logpolicy tailscale.com/log/logheap from tailscale.com/control/controlclient - tailscale.com/logpolicy from tailscale.com/cmd/tailscaled + tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+ tailscale.com/logtail from tailscale.com/logpolicy+ tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+ tailscale.com/logtail/filch from tailscale.com/logpolicy diff --git a/ipn/ipnserver/proxyconnect.go b/ipn/ipnserver/proxyconnect.go new file mode 100644 index 000000000..ead1eb794 --- /dev/null +++ b/ipn/ipnserver/proxyconnect.go @@ -0,0 +1,74 @@ +// Copyright (c) 2021 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 ipnserver + +import ( + "bufio" + "context" + "io" + "net" + "net/http" + "time" + + "tailscale.com/logpolicy" + "tailscale.com/types/logger" +) + +// handleProxyConnectConn handles a CONNECT request to +// log.tailscale.io (or whatever the configured log server is). This +// is intended for use by the Windows GUI client to log via when an +// exit node is in use, so the logs don't go out via the exit node and +// instead go directly, like tailscaled's. The dialer tried to do that +// in the unprivileged GUI by binding to a specific interface, but the +// "Internet Kill Switch" installed by tailscaled for exit nodes +// precludes that from working and instead the GUI fails to dial out. +// So, go through tailscaled (with a CONNECT request) instead. +func (s *Server) handleProxyConnectConn(ctx context.Context, br *bufio.Reader, c net.Conn, logf logger.Logf) { + defer c.Close() + + c.SetReadDeadline(time.Now().Add(5 * time.Second)) // should be long enough to send the HTTP headers + req, err := http.ReadRequest(br) + if err != nil { + logf("ReadRequest: %v", err) + return + } + c.SetReadDeadline(time.Time{}) + + if req.Method != "CONNECT" { + logf("ReadRequest: unexpected method %q, not CONNECT", req.Method) + return + } + + hostPort := req.RequestURI + logHost := logpolicy.LogHost() + allowed := net.JoinHostPort(logHost, "443") + if hostPort != allowed { + logf("invalid CONNECT target %q; want %q", hostPort, allowed) + io.WriteString(c, "HTTP/1.1 403 Forbidden\r\n\r\nBad CONNECT target.\n") + return + } + + tr := logpolicy.NewLogtailTransport(logHost) + back, err := tr.DialContext(ctx, "tcp", hostPort) + if err != nil { + logf("error CONNECT dialing %v: %v", hostPort, err) + io.WriteString(c, "HTTP/1.1 502 Fail\r\n\r\nConnect failure.\n") + return + } + defer back.Close() + + io.WriteString(c, "HTTP/1.1 200 OK\r\n\r\n") + + errc := make(chan error, 2) + go func() { + _, err := io.Copy(c, back) + errc <- err + }() + go func() { + _, err := io.Copy(back, br) + errc <- err + }() + <-errc +} diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 802a0a0fd..652911bd5 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -238,12 +238,28 @@ func bufferHasHTTPRequest(br *bufio.Reader) bool { mem.Contains(mem.B(peek), mem.S(" HTTP/")) } +// bufferIsConnect reports whether br looks like it's likely an HTTP +// CONNECT request. +// +// Invariant: br has already had at least 4 bytes Peek'ed. +func bufferIsConnect(br *bufio.Reader) bool { + peek, _ := br.Peek(br.Buffered()) + return mem.HasPrefix(mem.B(peek), mem.S("CONN")) +} + func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) { // First see if it's an HTTP request. br := bufio.NewReader(c) c.SetReadDeadline(time.Now().Add(time.Second)) br.Peek(4) c.SetReadDeadline(time.Time{}) + + // Handle logtail CONNECT requests early. (See docs on handleProxyConnectConn) + if bufferIsConnect(br) { + s.handleProxyConnectConn(ctx, br, c, logf) + return + } + isHTTPReq := bufferHasHTTPRequest(br) ci, err := s.addConn(c, isHTTPReq) diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go index c67557697..8bc0675f5 100644 --- a/logpolicy/logpolicy.go +++ b/logpolicy/logpolicy.go @@ -8,10 +8,12 @@ package logpolicy import ( + "bufio" "bytes" "context" "crypto/tls" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -40,6 +42,7 @@ import ( "tailscale.com/net/tlsdial" "tailscale.com/net/tshttpproxy" "tailscale.com/paths" + "tailscale.com/safesocket" "tailscale.com/smallzstd" "tailscale.com/types/logger" "tailscale.com/util/clientmetric" @@ -67,6 +70,15 @@ func getLogTarget() string { return getLogTargetOnce.v } +// LogHost returns the hostname only (without port) of the configured +// logtail server, or the default. +func LogHost() string { + if v := getLogTarget(); v != "" { + return v + } + return logtail.DefaultHost +} + // Config represents an instance of logs in a collection. type Config struct { Collection string @@ -616,6 +628,24 @@ func NewLogtailTransport(host string) *http.Transport { return c, nil } + if version.IsWindowsGUI() && strings.HasPrefix(netw, "tcp") { + if c, err := safesocket.Connect(safesocket.DefaultConnectionStrategy("")); err == nil { + fmt.Fprintf(c, "CONNECT %s HTTP/1.0\r\n\r\n", addr) + br := bufio.NewReader(c) + res, err := http.ReadResponse(br, nil) + if err == nil && res.StatusCode != 200 { + err = errors.New(res.Status) + } + if err != nil { + log.Printf("logtail: CONNECT response from tailscaled: %v", err) + c.Close() + } else { + log.Printf("logtail: connected via tailscaled") + return c, nil + } + } + } + // If we failed to dial, try again with bootstrap DNS. log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d) dnsCache := &dnscache.Resolver{ diff --git a/version/prop.go b/version/prop.go index 1d39ab589..fcb095062 100644 --- a/version/prop.go +++ b/version/prop.go @@ -62,3 +62,13 @@ func IsMacSysExt() bool { isMacSysExt.Store(v) return v } + +// IsWindowsGUI reports whether the current process is the Windows GUI. +func IsWindowsGUI() bool { + if runtime.GOOS != "windows" { + return false + } + exe, _ := os.Executable() + exe = filepath.Base(exe) + return strings.EqualFold(exe, "tailscale-ipn.exe") || strings.EqualFold(exe, "tailscale-ipn") +}