mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
logpolicy, ipn/ipnserver: connect to logtail via tailscaled when needed
This is 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. Fixes tailscale/corp#3169 Change-Id: I17a8efdc1d4b8fed53a29d1c19995592b651b215 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
5a9914a92f
commit
3dedcd1640
@ -183,7 +183,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/kube from tailscale.com/ipn
|
tailscale.com/kube from tailscale.com/ipn
|
||||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
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 from tailscale.com/logpolicy+
|
||||||
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
|
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
|
||||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||||
|
74
ipn/ipnserver/proxyconnect.go
Normal file
74
ipn/ipnserver/proxyconnect.go
Normal file
@ -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
|
||||||
|
}
|
@ -238,12 +238,28 @@ func bufferHasHTTPRequest(br *bufio.Reader) bool {
|
|||||||
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
|
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) {
|
func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||||
// First see if it's an HTTP request.
|
// First see if it's an HTTP request.
|
||||||
br := bufio.NewReader(c)
|
br := bufio.NewReader(c)
|
||||||
c.SetReadDeadline(time.Now().Add(time.Second))
|
c.SetReadDeadline(time.Now().Add(time.Second))
|
||||||
br.Peek(4)
|
br.Peek(4)
|
||||||
c.SetReadDeadline(time.Time{})
|
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)
|
isHTTPReq := bufferHasHTTPRequest(br)
|
||||||
|
|
||||||
ci, err := s.addConn(c, isHTTPReq)
|
ci, err := s.addConn(c, isHTTPReq)
|
||||||
|
@ -8,10 +8,12 @@
|
|||||||
package logpolicy
|
package logpolicy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -40,6 +42,7 @@
|
|||||||
"tailscale.com/net/tlsdial"
|
"tailscale.com/net/tlsdial"
|
||||||
"tailscale.com/net/tshttpproxy"
|
"tailscale.com/net/tshttpproxy"
|
||||||
"tailscale.com/paths"
|
"tailscale.com/paths"
|
||||||
|
"tailscale.com/safesocket"
|
||||||
"tailscale.com/smallzstd"
|
"tailscale.com/smallzstd"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/clientmetric"
|
"tailscale.com/util/clientmetric"
|
||||||
@ -67,6 +70,15 @@ func getLogTarget() string {
|
|||||||
return getLogTargetOnce.v
|
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.
|
// Config represents an instance of logs in a collection.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Collection string
|
Collection string
|
||||||
@ -616,6 +628,24 @@ func NewLogtailTransport(host string) *http.Transport {
|
|||||||
return c, nil
|
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.
|
// If we failed to dial, try again with bootstrap DNS.
|
||||||
log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
|
log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
|
||||||
dnsCache := &dnscache.Resolver{
|
dnsCache := &dnscache.Resolver{
|
||||||
|
@ -62,3 +62,13 @@ func IsMacSysExt() bool {
|
|||||||
isMacSysExt.Store(v)
|
isMacSysExt.Store(v)
|
||||||
return 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")
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user