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 <bradfitz@tailscale.com>
pull/2961/head
Brad Fitzpatrick 3 years ago committed by Brad Fitzpatrick
parent 29a8fb45d3
commit a7cb241db1

@ -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+

@ -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
})
}

@ -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:<secret-name>' 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
}

@ -25,6 +25,7 @@ import (
_ "net"
_ "net/http"
_ "net/http/httptrace"
_ "net/http/httputil"
_ "net/http/pprof"
_ "net/url"
_ "os"

@ -25,6 +25,7 @@ import (
_ "net"
_ "net/http"
_ "net/http/httptrace"
_ "net/http/httputil"
_ "net/http/pprof"
_ "net/url"
_ "os"

@ -25,6 +25,7 @@ import (
_ "net"
_ "net/http"
_ "net/http/httptrace"
_ "net/http/httputil"
_ "net/http/pprof"
_ "net/url"
_ "os"

@ -25,6 +25,7 @@ import (
_ "net"
_ "net/http"
_ "net/http/httptrace"
_ "net/http/httputil"
_ "net/http/pprof"
_ "net/url"
_ "os"

@ -29,6 +29,7 @@ import (
_ "net"
_ "net/http"
_ "net/http/httptrace"
_ "net/http/httputil"
_ "net/http/pprof"
_ "net/url"
_ "os"

Loading…
Cancel
Save