summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--cmd/tailscaled/proxy.go48
-rw-r--r--ipn/ipnserver/proxyconnect.go48
-rw-r--r--net/httpconnect/httpconnect.go106
3 files changed, 117 insertions, 85 deletions
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
- }
-
- 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
+ connect := &httpconnect.Connect{
+ Dialer: logpolicy.NewLogtailTransport(logHost).DialContext,
+ AllowedURI: net.JoinHostPort(logHost, "443"),
}
- 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
+}