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 @@ var args struct { 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 @@ import ( _ "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 @@ import ( _ "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 @@ import ( _ "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 @@ import ( _ "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 @@ import ( _ "net" _ "net/http" _ "net/http/httptrace" + _ "net/http/httputil" _ "net/http/pprof" _ "net/url" _ "os"