From 478219eeb50ca7d12ed505247350426172eae372 Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Wed, 8 Mar 2023 12:33:47 -0800 Subject: [PATCH] net/httpconnect: HTTP CONNECT implementation Turns out we had two of these. Factor them out, and add auth support. Signed-off-by: David Crawshaw --- cmd/tailscaled/proxy.go | 48 ++------------- ipn/ipnserver/proxyconnect.go | 48 ++------------- net/httpconnect/httpconnect.go | 106 +++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 85 deletions(-) create mode 100644 net/httpconnect/httpconnect.go diff --git a/cmd/tailscaled/proxy.go b/cmd/tailscaled/proxy.go index a91c62bfa..0b25f340b 100644 --- a/cmd/tailscaled/proxy.go +++ b/cmd/tailscaled/proxy.go @@ -9,11 +9,12 @@ package main import ( "context" - "io" "net" "net/http" "net/http/httputil" "strings" + + "tailscale.com/net/httpconnect" ) // httpProxyHandler returns an HTTP proxy http.Handler using the @@ -25,6 +26,9 @@ func httpProxyHandler(dialer func(ctx context.Context, netw, addr string) (net.C DialContext: dialer, }, } + connect := &httpconnect.Connect{ + Dialer: dialer, + } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "CONNECT" { backURL := r.RequestURI @@ -35,46 +39,6 @@ func httpProxyHandler(dialer func(ctx context.Context, netw, addr string) (net.C 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 + connect.Handle(w, r) }) } diff --git a/ipn/ipnserver/proxyconnect.go b/ipn/ipnserver/proxyconnect.go index eb8c55991..cab603ba2 100644 --- a/ipn/ipnserver/proxyconnect.go +++ b/ipn/ipnserver/proxyconnect.go @@ -6,11 +6,11 @@ package ipnserver import ( - "io" "net" "net/http" "tailscale.com/logpolicy" + "tailscale.com/net/httpconnect" ) // handleProxyConnectConn handles a CONNECT request to @@ -23,51 +23,13 @@ import ( // 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(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() if r.Method != "CONNECT" { panic("[unexpected] miswired") } - - hostPort := r.RequestURI logHost := logpolicy.LogHost() - allowed := net.JoinHostPort(logHost, "443") - if hostPort != allowed { - s.logf("invalid CONNECT target %q; want %q", hostPort, allowed) - http.Error(w, "Bad CONNECT target.", http.StatusForbidden) - return + connect := &httpconnect.Connect{ + Dialer: logpolicy.NewLogtailTransport(logHost).DialContext, + AllowedURI: net.JoinHostPort(logHost, "443"), } - - tr := logpolicy.NewLogtailTransport(logHost) - back, err := tr.DialContext(ctx, "tcp", hostPort) - if err != nil { - s.logf("error CONNECT dialing %v: %v", hostPort, err) - http.Error(w, "Connect failure", http.StatusBadGateway) - return - } - defer back.Close() - - hj, ok := w.(http.Hijacker) - if !ok { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - c, br, err := hj.Hijack() - if err != nil { - s.logf("CONNECT hijack: %v", err) - return - } - defer c.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 + connect.Handle(w, r) } diff --git a/net/httpconnect/httpconnect.go b/net/httpconnect/httpconnect.go new file mode 100644 index 000000000..8ca9a2c55 --- /dev/null +++ b/net/httpconnect/httpconnect.go @@ -0,0 +1,106 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Package httpconnect implements HTTP CONNECT request proxying. +package httpconnect + +import ( + "context" + "encoding/base64" + "io" + "net" + "net/http" + + "tailscale.com/types/logger" +) + +type Connect struct { + Dialer func(ctx context.Context, netw, addr string) (net.Conn, error) + Logf logger.Logf + AllowedURI string // if set, requests can only connect to this URI + + // Username and Password, if set, are the required proxy auth credentials. + Username, Password string + + authHeader string // encoded Username+Password for header comparison +} + +func (c *Connect) uriAllowed(w http.ResponseWriter, r *http.Request) bool { + if c.AllowedURI == "" { + return true + } + if r.RequestURI == c.AllowedURI { + return true + } + if c.Logf != nil { + c.Logf("invalid CONNECT target %q; want %q", r.RequestURI, c.AllowedURI) + } + http.Error(w, "Bad CONNECT target.", http.StatusForbidden) + return false +} + +func (c *Connect) authorized(w http.ResponseWriter, r *http.Request) bool { + if c.Username == "" && c.Password == "" { + return true + } + if c.authHeader == "" { + c.authHeader = "Basic " + base64.StdEncoding.EncodeToString([]byte(c.Username+":"+c.Password)) + } + if r.Header.Get("Proxy-Authorization") == c.authHeader { + return true + } + w.Header().Set("Proxy-Authenticate", `Basic, realm="tailnet"`) + http.Error(w, "Proxy Authentication Required", 407) + return false +} + +func (c *Connect) Handle(w http.ResponseWriter, r *http.Request) { + if r.Method != "CONNECT" { + panic("[unexpected] miswired") + } + if !c.uriAllowed(w, r) || !c.authorized(w, r) { + return + } + + dst := r.RequestURI + conn, err := c.Dialer(r.Context(), "tcp", dst) + if err != nil { + if c.Logf != nil { + c.Logf("error CONNECT dialing %v: %v", dst, err) + } + w.Header().Set("Tailscale-Connect-Error", err.Error()) + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + defer conn.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, conn) + errc <- err + }() + go func() { + _, err := io.Copy(conn, clientSrc) + errc <- err + }() + <-errc +}