diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index 98371393d..e2caaa47c 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -41,6 +41,13 @@ import ( "tailscale.com/types/tkatype" ) +// DialFunc is any function that dials the given address. +type DialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) + +// h2cTransport returns nil on platforms where H2C ("cleartext" HTTP/2) +// support for the LocalAPI is not implemented. +var h2cTransport = func(DialFunc) http.RoundTripper { return nil } + // defaultLocalClient is the default LocalClient when using the legacy // package-level functions. var defaultLocalClient LocalClient @@ -58,7 +65,7 @@ var defaultLocalClient LocalClient type LocalClient struct { // Dial optionally specifies an alternate func that connects to the local // machine's tailscaled or equivalent. If nil, a default is used. - Dial func(ctx context.Context, network, addr string) (net.Conn, error) + Dial DialFunc // Socket specifies an alternate path to the local Tailscale socket. // If empty, a platform-specific default is used. @@ -77,6 +84,9 @@ type LocalClient struct { // different operating system, such as in integration tests. OmitAuth bool + // AllowH2C enables H2C ("cleartext" HTTP/2) if supported on the current platform. + AllowH2C bool + // tsClient does HTTP requests to the local Tailscale daemon. // It's lazily initialized on first use. tsClient *http.Client @@ -90,7 +100,7 @@ func (lc *LocalClient) socket() string { return paths.DefaultTailscaledSocket() } -func (lc *LocalClient) dialer() func(ctx context.Context, network, addr string) (net.Conn, error) { +func (lc *LocalClient) dialer() DialFunc { if lc.Dial != nil { return lc.Dial } @@ -114,6 +124,17 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string) return safesocket.ConnectContext(ctx, lc.socket()) } +// transport returns the HTTP transport to be used when making requests +// to the local machine's Tailscale daemon. +func (lc *LocalClient) transport() http.RoundTripper { + if lc.AllowH2C { + if t := h2cTransport(lc.dialer()); t != nil { + return t + } + } + return &http.Transport{DialContext: lc.dialer()} +} + // DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon. // // URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4. @@ -126,11 +147,7 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string) func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error) { req.Header.Set("Tailscale-Cap", strconv.Itoa(int(tailcfg.CurrentCapabilityVersion))) lc.tsClientOnce.Do(func() { - lc.tsClient = &http.Client{ - Transport: &http.Transport{ - DialContext: lc.dialer(), - }, - } + lc.tsClient = &http.Client{Transport: lc.transport()} }) if !lc.OmitAuth { if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil { diff --git a/client/tailscale/localclient_h2c.go b/client/tailscale/localclient_h2c.go new file mode 100644 index 000000000..1b20bbe0d --- /dev/null +++ b/client/tailscale/localclient_h2c.go @@ -0,0 +1,31 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Enabling H2C for LocalAPI is not Windows-specific. +// However, Windows is expected to benefit the most +// due to additional, potentially slow authentication steps +// performed for each new named pipe connection. +// As an experiment, we are enabling it on Windows first. +//go:build windows + +package tailscale + +import ( + "context" + "crypto/tls" + "net" + "net/http" + + "golang.org/x/net/http2" +) + +func init() { + h2cTransport = func(dialer DialFunc) http.RoundTripper { + return &http2.Transport{ + AllowHTTP: true, + DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { + return dialer(ctx, network, addr) + }, + } + } +} diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 53b263d03..b9aad9e47 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -189,9 +189,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa golang.org/x/exp/maps from tailscale.com/util/syspolicy/setting L golang.org/x/net/bpf from github.com/mdlayher/netlink+ golang.org/x/net/dns/dnsmessage from net+ - golang.org/x/net/http/httpguts from net/http + golang.org/x/net/http/httpguts from net/http+ golang.org/x/net/http/httpproxy from net/http+ - golang.org/x/net/http2/hpack from net/http + W golang.org/x/net/http2 from tailscale.com/client/tailscale + golang.org/x/net/http2/hpack from net/http+ golang.org/x/net/idna from golang.org/x/crypto/acme/autocert+ golang.org/x/net/proxy from tailscale.com/net/netns D golang.org/x/net/route from net+ diff --git a/cmd/tailscale/cli/cli.go b/cmd/tailscale/cli/cli.go index efbdd3e40..cde0f70e5 100644 --- a/cmd/tailscale/cli/cli.go +++ b/cmd/tailscale/cli/cli.go @@ -79,7 +79,8 @@ func CleanUpArgs(args []string) []string { } var localClient = tailscale.LocalClient{ - Socket: paths.DefaultTailscaledSocket(), + AllowH2C: true, + Socket: paths.DefaultTailscaledSocket(), } // Run runs the CLI. The args do not include the binary name. diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index eed37c7d4..7489cdc29 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -451,7 +451,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de golang.org/x/net/http/httpguts from golang.org/x/net/http2+ golang.org/x/net/http/httpproxy from net/http+ golang.org/x/net/http2 from golang.org/x/net/http2/h2c+ - golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal + golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal+ golang.org/x/net/http2/hpack from golang.org/x/net/http2+ golang.org/x/net/icmp from tailscale.com/net/ping+ golang.org/x/net/idna from golang.org/x/net/http/httpguts+ diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 25c672e2e..e52540463 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -33,6 +33,10 @@ import ( "tailscale.com/util/systemd" ) +// addH2C is a no-op on platforms where the LocalAPI +// does not support H2C ("cleartext" HTTP/2). +var addH2C = func(*http.Server) {} + // Server is an IPN backend and its set of 0 or more active localhost // TCP or unix socket connections talking to that backend. type Server struct { @@ -515,6 +519,7 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error { IdleTimeout: 5 * time.Second, ErrorLog: logger.StdLogger(logger.WithPrefix(s.logf, "ipnserver: ")), } + addH2C(hs) if err := hs.Serve(ln); err != nil { if err := ctx.Err(); err != nil { return err diff --git a/ipn/ipnserver/server_h2c.go b/ipn/ipnserver/server_h2c.go new file mode 100644 index 000000000..31270ea90 --- /dev/null +++ b/ipn/ipnserver/server_h2c.go @@ -0,0 +1,29 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Enabling H2C for LocalAPI is not Windows-specific. +// However, Windows is expected to benefit the most +// due to additional, potentially slow authentication steps +// performed for each new named pipe connection. +// As an experiment, we are enabling it on Windows first. +//go:build windows + +package ipnserver + +import ( + "net/http" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +func init() { + addH2C = func(s *http.Server) { + h2s := &http2.Server{} + s.Handler = h2c.NewHandler(s.Handler, h2s) + // [http2.ConfigureServer] sets up a server shutdown handler that gracefully + // closes connections when [http.Server.Shutdown] is called. + // Otherwise, it leaks goroutines. + http2.ConfigureServer(s, h2s) + } +}