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 <nickk@tailscale.com>
This commit is contained in:
Nick Khyl 2024-09-04 00:20:19 -05:00
parent c4d0237e5c
commit 6696b8ee62
7 changed files with 95 additions and 11 deletions

View File

@ -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 {

View File

@ -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)
},
}
}
}

View File

@ -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+

View File

@ -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.

View File

@ -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+

View File

@ -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

View File

@ -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)
}
}