diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index f112cdec7..e1eb0cf9e 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -170,7 +170,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/client/tailscale from tailscale.com/derp tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+ tailscale.com/cmd/tailscaled/childproc from tailscale.com/cmd/tailscaled+ + tailscale.com/control/controlbase from tailscale.com/control/controlclient+ tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+ + tailscale.com/control/controlhttp from tailscale.com/control/controlclient tailscale.com/control/controlknobs from tailscale.com/control/controlclient+ tailscale.com/derp from tailscale.com/derp/derphttp+ tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+ @@ -276,7 +278,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router golang.org/x/crypto/acme from tailscale.com/ipn/localapi golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box - golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device + golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device+ LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+ golang.org/x/crypto/chacha20poly1305 from crypto/tls+ @@ -284,7 +286,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+ golang.org/x/crypto/curve25519 from crypto/tls+ LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh - golang.org/x/crypto/hkdf from crypto/tls + golang.org/x/crypto/hkdf from crypto/tls+ golang.org/x/crypto/nacl/box from tailscale.com/types/key golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+ @@ -303,7 +305,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de golang.org/x/net/proxy from tailscale.com/net/netns D golang.org/x/net/route from net+ golang.org/x/sync/errgroup from github.com/tailscale/goupnp/httpu+ - golang.org/x/sync/singleflight from tailscale.com/net/dnscache + golang.org/x/sync/singleflight from tailscale.com/net/dnscache+ golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+ LD golang.org/x/sys/unix from github.com/insomniacslk/dhcp/interfaces+ W golang.org/x/sys/windows from github.com/go-ole/go-ole+ diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index d3643f719..9fd37559c 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -27,6 +27,7 @@ import ( "time" "go4.org/mem" + "golang.org/x/sync/singleflight" "inet.af/netaddr" "tailscale.com/control/controlknobs" "tailscale.com/envknob" @@ -73,6 +74,9 @@ type Direct struct { serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key serverNoiseKey key.MachinePublic + sfGroup singleflight.Group // protects noiseClient creation. + noiseClient *noiseClient + persist persist.Persist authKey string tryingNewKey key.NodePrivate @@ -203,6 +207,19 @@ func NewDirect(opts Options) (*Direct, error) { return c, nil } +// Close closes the underlying Noise connection(s). +func (c *Direct) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + if c.noiseClient != nil { + if err := c.noiseClient.Close(); err != nil { + return err + } + } + c.noiseClient = nil + return nil +} + // SetHostinfo clones the provided Hostinfo and remembers it for the // next update. It reports whether the Hostinfo has changed. func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool { @@ -1204,6 +1221,39 @@ func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- } } +// getNoiseClient returns the noise client, creating one if one doesn't exist. +func (c *Direct) getNoiseClient() (*noiseClient, error) { + c.mu.Lock() + serverNoiseKey := c.serverNoiseKey + nc := c.noiseClient + c.mu.Unlock() + if serverNoiseKey.IsZero() { + return nil, errors.New("zero serverNoiseKey") + } + if nc != nil { + return nc, nil + } + np, err, _ := c.sfGroup.Do("noise", func() (interface{}, error) { + k, err := c.getMachinePrivKey() + if err != nil { + return nil, err + } + + nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL) + if err != nil { + return nil, err + } + c.mu.Lock() + defer c.mu.Unlock() + c.noiseClient = nc + return nc, nil + }) + if err != nil { + return nil, err + } + return np.(*noiseClient), nil +} + // SetDNS sends the SetDNSRequest request to the control plane server, // requesting a DNS record be created or updated. func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err error) { diff --git a/control/controlclient/noise.go b/control/controlclient/noise.go new file mode 100644 index 000000000..ca3c5ecf7 --- /dev/null +++ b/control/controlclient/noise.go @@ -0,0 +1,141 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package controlclient + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/url" + "sync" + "time" + + "golang.org/x/net/http2" + "tailscale.com/control/controlbase" + "tailscale.com/control/controlhttp" + "tailscale.com/types/key" + "tailscale.com/util/multierr" +) + +// noiseConn is a wrapper around controlbase.Conn. +// It allows attaching an ID to a connection to allow +// cleaning up references in the pool when the connection +// is closed. +type noiseConn struct { + *controlbase.Conn + id int + pool *noiseClient +} + +func (c *noiseConn) Close() error { + if err := c.Conn.Close(); err != nil { + return err + } + c.pool.connClosed(c.id) + return nil +} + +// noiseClient provides a http.Client to connect to tailcontrol over +// the ts2021 protocol. +type noiseClient struct { + *http.Client // HTTP client used to talk to tailcontrol + privKey key.MachinePrivate + serverPubKey key.MachinePublic + serverHost string // the host:port part of serverURL + + // mu only protects the following variables. + mu sync.Mutex + nextID int + connPool map[int]*noiseConn // active connections not yet closed; see noiseConn.Close +} + +// newNoiseClient returns a new noiseClient for the provided server and machine key. +// serverURL is of the form https://: (no trailing slash). +func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string) (*noiseClient, error) { + u, err := url.Parse(serverURL) + if err != nil { + return nil, err + } + var host string + if u.Port() != "" { + // If there is an explicit port specified use it. + host = u.Host + } else { + // Otherwise, controlhttp.Dial expects an http endpoint. + host = fmt.Sprintf("%v:80", u.Hostname()) + } + np := &noiseClient{ + serverPubKey: serverPubKey, + privKey: priKey, + serverHost: host, + } + + // Create a new http.Client which dials out using nc.Dial. + np.Client = &http.Client{ + Transport: &http2.Transport{ + ReadIdleTimeout: time.Minute, + DialTLS: np.dial, + }, + } + + return np, nil +} + +// connClosed removes the connection with the provided ID from the pool +// of active connections. +func (nc *noiseClient) connClosed(id int) { + nc.mu.Lock() + defer nc.mu.Unlock() + delete(nc.connPool, id) +} + +// Close closes all the underlying noise connections. +// It is a no-op and returns nil if the connection is already closed. +func (nc *noiseClient) Close() error { + nc.mu.Lock() + conns := nc.connPool + nc.connPool = nil + nc.mu.Unlock() + + var errors []error + for _, c := range conns { + if err := c.Close(); err != nil { + errors = append(errors, err) + } + } + return multierr.New(errors...) +} + +// dial opens a new connection to tailcontrol, fetching the server noise key +// if not cached. It implements the signature needed by http2.Transport.DialTLS +// but ignores all params as it only dials out to the server the noiseClient was +// created for. +func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) { + nc.mu.Lock() + connID := nc.nextID + if nc.connPool == nil { + nc.connPool = make(map[int]*noiseConn) + } + nc.nextID++ + nc.mu.Unlock() + + // Timeout is a little arbitrary, but plenty long enough for even the + // highest latency links. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + conn, err := controlhttp.Dial(ctx, nc.serverHost, nc.privKey, nc.serverPubKey) + if err != nil { + return nil, err + } + + nc.mu.Lock() + defer nc.mu.Unlock() + ncc := &noiseConn{Conn: conn, id: connID, pool: nc} + nc.connPool[ncc.id] = ncc + return ncc, nil +}