From a7cb241db1edbb3f433eb56f0c6a7a6b5043f0c4 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 28 Sep 2021 10:16:05 -0700 Subject: [PATCH] cmd/tailscaled: add support for running an HTTP proxy This adds support for tailscaled to be an HTTP proxy server. It shares the same backend dialing code as the SOCK5 server, but the client protocol is HTTP (including CONNECT), rather than SOCKS. Fixes #2289 Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/depaware.txt | 2 +- cmd/tailscaled/proxy.go | 79 +++++++++++++++++++ cmd/tailscaled/tailscaled.go | 49 ++++++++---- .../tailscaled_deps_test_darwin.go | 1 + .../tailscaled_deps_test_freebsd.go | 1 + .../integration/tailscaled_deps_test_linux.go | 1 + .../tailscaled_deps_test_openbsd.go | 1 + .../tailscaled_deps_test_windows.go | 1 + 8 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 cmd/tailscaled/proxy.go diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 108e8e3a9..b9606c7a3 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -277,7 +277,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de net from crypto/tls+ net/http from expvar+ net/http/httptrace from github.com/tcnksm/go-httpstat+ - net/http/httputil from tailscale.com/ipn/localapi + net/http/httputil from tailscale.com/ipn/localapi+ net/http/internal from net/http+ net/http/pprof from tailscale.com/cmd/tailscaled+ net/textproto from golang.org/x/net/http/httpguts+ diff --git a/cmd/tailscaled/proxy.go b/cmd/tailscaled/proxy.go new file mode 100644 index 000000000..cbd6ab6e1 --- /dev/null +++ b/cmd/tailscaled/proxy.go @@ -0,0 +1,79 @@ +// 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. + +// HTTP proxy code + +package main + +import ( + "context" + "io" + "net" + "net/http" + "net/http/httputil" + "strings" +) + +// httpProxyHandler returns an HTTP proxy http.Handler using the +// provided backend dialer. +func httpProxyHandler(dialer func(ctx context.Context, netw, addr string) (net.Conn, error)) http.Handler { + rp := &httputil.ReverseProxy{ + Director: func(r *http.Request) {}, // no change + Transport: &http.Transport{ + DialContext: dialer, + }, + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "CONNECT" { + backURL := r.RequestURI + if strings.HasPrefix(backURL, "/") || backURL == "*" { + http.Error(w, "bogus RequestURI; must be absolute URL or CONNECT", 400) + return + } + rp.ServeHTTP(w, r) + return + } + + // CONNECT support: + + dst := r.RequestURI + c, err := dialer(r.Context(), "tcp", dst) + if err != nil { + w.Header().Set("Tailscale-Connect-Error", err.Error()) + http.Error(w, err.Error(), 500) + return + } + defer c.Close() + + cc, ccbuf, err := w.(http.Hijacker).Hijack() + if err != nil { + http.Error(w, err.Error(), 500) + return + } + defer cc.Close() + + io.WriteString(cc, "HTTP/1.1 200 OK\r\n\r\n") + + var clientSrc io.Reader = ccbuf + if ccbuf.Reader.Buffered() == 0 { + // In the common case (with no + // buffered data), read directly from + // the underlying client connection to + // save some memory, letting the + // bufio.Reader/Writer get GC'ed. + clientSrc = cc + } + + errc := make(chan error, 1) + go func() { + _, err := io.Copy(cc, c) + errc <- err + }() + go func() { + _, err := io.Copy(c, clientSrc) + errc <- err + }() + <-errc + }) +} diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index f6d6ae11b..8f449c700 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -82,6 +82,7 @@ func defaultTunName() string { birdSocketPath string verbose int socksAddr string // listen address for SOCKS5 server + httpProxyAddr string // listen address for HTTP proxy server } var ( @@ -110,6 +111,7 @@ func main() { flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit") flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server") flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`) + flag.StringVar(&args.httpProxyAddr, "http-proxy-server", "", `optional [ip]:port to run an HTTP proxy (e.g. "localhost:8080")`) flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`) flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select") flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file; use 'kube:' to use Kubernetes secrets") @@ -278,19 +280,8 @@ func run() error { } pol.Logtail.SetLinkMonitor(linkMon) - var socksListener net.Listener - if args.socksAddr != "" { - var err error - socksListener, err = net.Listen("tcp", args.socksAddr) - if err != nil { - log.Fatalf("SOCKS5 listener: %v", err) - } - if strings.HasSuffix(args.socksAddr, ":0") { - // Log kernel-selected port number so integration tests - // can find it portably. - log.Printf("SOCKS5 listening on %v", socksListener.Addr()) - } - } + socksListener := mustStartTCPListener("SOCKS5", args.socksAddr) + httpProxyListener := mustStartTCPListener("HTTP proxy", args.httpProxyAddr) e, useNetstack, err := createEngine(logf, linkMon) if err != nil { @@ -304,11 +295,19 @@ func run() error { ns = mustStartNetstack(logf, e, onlySubnets) } - if socksListener != nil { + if socksListener != nil || httpProxyListener != nil { srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns) - go func() { - log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener)) - }() + if httpProxyListener != nil { + hs := &http.Server{Handler: httpProxyHandler(srv.Dialer)} + go func() { + log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener)) + }() + } + if socksListener != nil { + go func() { + log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener)) + }() + } } e = wgengine.NewWatchdog(e) @@ -468,3 +467,19 @@ func mustStartNetstack(logf logger.Logf, e wgengine.Engine, onlySubnets bool) *n } return ns } + +func mustStartTCPListener(name, addr string) net.Listener { + if addr == "" { + return nil + } + ln, err := net.Listen("tcp", addr) + if err != nil { + log.Fatalf("%v listener: %v", name, err) + } + if strings.HasSuffix(addr, ":0") { + // Log kernel-selected port number so integration tests + // can find it portably. + log.Printf("%v listening on %v", name, ln.Addr()) + } + return ln +} diff --git a/tstest/integration/tailscaled_deps_test_darwin.go b/tstest/integration/tailscaled_deps_test_darwin.go index 32b389388..fd6c743fb 100644 --- a/tstest/integration/tailscaled_deps_test_darwin.go +++ b/tstest/integration/tailscaled_deps_test_darwin.go @@ -25,6 +25,7 @@ _ "net" _ "net/http" _ "net/http/httptrace" + _ "net/http/httputil" _ "net/http/pprof" _ "net/url" _ "os" diff --git a/tstest/integration/tailscaled_deps_test_freebsd.go b/tstest/integration/tailscaled_deps_test_freebsd.go index d76d91ed6..1a83ef516 100644 --- a/tstest/integration/tailscaled_deps_test_freebsd.go +++ b/tstest/integration/tailscaled_deps_test_freebsd.go @@ -25,6 +25,7 @@ _ "net" _ "net/http" _ "net/http/httptrace" + _ "net/http/httputil" _ "net/http/pprof" _ "net/url" _ "os" diff --git a/tstest/integration/tailscaled_deps_test_linux.go b/tstest/integration/tailscaled_deps_test_linux.go index d76d91ed6..1a83ef516 100644 --- a/tstest/integration/tailscaled_deps_test_linux.go +++ b/tstest/integration/tailscaled_deps_test_linux.go @@ -25,6 +25,7 @@ _ "net" _ "net/http" _ "net/http/httptrace" + _ "net/http/httputil" _ "net/http/pprof" _ "net/url" _ "os" diff --git a/tstest/integration/tailscaled_deps_test_openbsd.go b/tstest/integration/tailscaled_deps_test_openbsd.go index d76d91ed6..1a83ef516 100644 --- a/tstest/integration/tailscaled_deps_test_openbsd.go +++ b/tstest/integration/tailscaled_deps_test_openbsd.go @@ -25,6 +25,7 @@ _ "net" _ "net/http" _ "net/http/httptrace" + _ "net/http/httputil" _ "net/http/pprof" _ "net/url" _ "os" diff --git a/tstest/integration/tailscaled_deps_test_windows.go b/tstest/integration/tailscaled_deps_test_windows.go index 3d3cf7317..8dd2c4bdc 100644 --- a/tstest/integration/tailscaled_deps_test_windows.go +++ b/tstest/integration/tailscaled_deps_test_windows.go @@ -29,6 +29,7 @@ _ "net" _ "net/http" _ "net/http/httptrace" + _ "net/http/httputil" _ "net/http/pprof" _ "net/url" _ "os"