From 6696b8ee6230c7ca87f42678f98df55a51c8ee87 Mon Sep 17 00:00:00 2001 From: Nick Khyl Date: Wed, 4 Sep 2024 00:20:19 -0500 Subject: [PATCH] client/tailscale,ipn/ipnserver: use h2c (for http/2 multiplexing) for LocalAPI on Windows This reduces the number of named pipe instances to one per client, allows us to authenticate the user on the other side of the named pipe just once, and helps identify a given connection (and all associated requests) without relying on the PID and start time of the client process. Updates tailscale/corp#18342 Signed-off-by: Nick Khyl --- client/tailscale/localclient.go | 31 ++++++++++++++++++++++------- client/tailscale/localclient_h2c.go | 31 +++++++++++++++++++++++++++++ cmd/derper/depaware.txt | 5 +++-- cmd/tailscale/cli/cli.go | 3 ++- cmd/tailscaled/depaware.txt | 2 +- ipn/ipnserver/server.go | 5 +++++ ipn/ipnserver/server_h2c.go | 29 +++++++++++++++++++++++++++ 7 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 client/tailscale/localclient_h2c.go create mode 100644 ipn/ipnserver/server_h2c.go 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) + } +}