@ -25,6 +25,7 @@ import (
"sync"
"sync"
"time"
"time"
"golang.org/x/net/http2"
"tailscale.com/ipn"
"tailscale.com/ipn"
"tailscale.com/logtail/backoff"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netutil"
"tailscale.com/net/netutil"
@ -35,6 +36,11 @@ import (
"tailscale.com/version"
"tailscale.com/version"
)
)
const (
contentTypeHeader = "Content-Type"
grpcBaseContentType = "application/grpc"
)
// ErrETagMismatch signals that the given
// ErrETagMismatch signals that the given
// If-Match header does not match with the
// If-Match header does not match with the
// current etag of a resource.
// current etag of a resource.
@ -534,23 +540,64 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
// proxyHandlerForBackend creates a new HTTP reverse proxy for a particular backend that
// proxyHandlerForBackend creates a new HTTP reverse proxy for a particular backend that
// we serve requests for. `backend` is a HTTPHandler.Proxy string (url, hostport or just port).
// we serve requests for. `backend` is a HTTPHandler.Proxy string (url, hostport or just port).
func ( b * LocalBackend ) proxyHandlerForBackend ( backend string ) ( * httputil . ReverseProxy , error ) {
func ( b * LocalBackend ) proxyHandlerForBackend ( backend string ) ( http . Handler , error ) {
targetURL , insecure := expandProxyArg ( backend )
targetURL , insecure := expandProxyArg ( backend )
u , err := url . Parse ( targetURL )
u , err := url . Parse ( targetURL )
if err != nil {
if err != nil {
return nil , fmt . Errorf ( "invalid url %s: %w" , targetURL , err )
return nil , fmt . Errorf ( "invalid url %s: %w" , targetURL , err )
}
}
rp := & httputil . ReverseProxy {
p := & reverseProxy {
Rewrite : func ( r * httputil . ProxyRequest ) {
logf : b . logf ,
r . SetURL ( u )
url : u ,
r . Out . Host = r . In . Host
insecure : insecure ,
addProxyForwardedHeaders ( r )
backend : backend ,
b . addTailscaleIdentityHeaders ( r )
lb : b ,
} ,
}
Transport : & http . Transport {
return p , nil
DialContext : b . dialer . SystemDial ,
}
// reverseProxy is a proxy that forwards a request to a backend host
// (preconfigured via ipn.ServeConfig). If the host is configured with
// http+insecure prefix, connection between proxy and backend will be over
// insecure TLS. If the backend host has a http prefix and the incoming request
// has application/grpc content type header, the connection will be over h2c.
// Otherwise standard Go http transport will be used.
type reverseProxy struct {
logf logger . Logf
url * url . URL
insecure bool
backend string
lb * LocalBackend
}
func ( rp * reverseProxy ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
p := & httputil . ReverseProxy { Rewrite : func ( r * httputil . ProxyRequest ) {
r . SetURL ( rp . url )
r . Out . Host = r . In . Host
addProxyForwardedHeaders ( r )
rp . lb . addTailscaleIdentityHeaders ( r )
} ,
}
// There is no way to autodetect h2c as per RFC 9113
// https://datatracker.ietf.org/doc/html/rfc9113#name-starting-http-2.
// However, we assume that http:// proxy prefix in combination with the
// protoccol being HTTP/2 is sufficient to detect h2c for our needs. Only use this for
// gRPC to fix a known problem pf plaintext gRPC backends
if rp . shouldProxyViaH2C ( r ) {
rp . logf ( "received a proxy request for plaintext gRPC" )
p . Transport = & http2 . Transport {
AllowHTTP : true ,
DialTLSContext : func ( ctx context . Context , network string , addr string , _ * tls . Config ) ( net . Conn , error ) {
return rp . lb . dialer . SystemDial ( ctx , "tcp" , rp . url . Host )
} ,
}
} else {
p . Transport = & http . Transport {
DialContext : rp . lb . dialer . SystemDial ,
TLSClientConfig : & tls . Config {
TLSClientConfig : & tls . Config {
InsecureSkipVerify : insecure ,
InsecureSkipVerify : rp. insecure,
} ,
} ,
// Values for the following parameters have been copied from http.DefaultTransport.
// Values for the following parameters have been copied from http.DefaultTransport.
ForceAttemptHTTP2 : true ,
ForceAttemptHTTP2 : true ,
@ -558,9 +605,27 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.Reverse
IdleConnTimeout : 90 * time . Second ,
IdleConnTimeout : 90 * time . Second ,
TLSHandshakeTimeout : 10 * time . Second ,
TLSHandshakeTimeout : 10 * time . Second ,
ExpectContinueTimeout : 1 * time . Second ,
ExpectContinueTimeout : 1 * time . Second ,
} ,
}
}
}
return rp , nil
p . ServeHTTP ( w , r )
}
// This is not a generally reliable way how to determine whether a request is
// for a h2c server, but sufficient for our particular use case.
func ( rp * reverseProxy ) shouldProxyViaH2C ( r * http . Request ) bool {
contentType := r . Header . Get ( contentTypeHeader )
return r . ProtoMajor == 2 && strings . HasPrefix ( rp . backend , "http://" ) && isGRPCContentType ( contentType )
}
// isGRPC accepts an HTTP request's content type header value and determines
// whether this is gRPC content. grpc-go considers a value that equals
// application/grpc or has a prefix of application/grpc+ or application/grpc; a
// valid grpc content type header.
// https://github.com/grpc/grpc-go/blob/v1.60.0-dev/internal/grpcutil/method.go#L41-L78
func isGRPCContentType ( contentType string ) bool {
s , ok := strings . CutPrefix ( contentType , grpcBaseContentType )
return ok && len ( s ) == 0 || s [ 0 ] == '+' || s [ 0 ] == ';'
}
}
func addProxyForwardedHeaders ( r * httputil . ProxyRequest ) {
func addProxyForwardedHeaders ( r * httputil . ProxyRequest ) {