control/controlclient: add Noise client

Updates #3488

Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
Maisem Ali 2022-03-07 15:32:53 -08:00 committed by Maisem Ali
parent 26f27a620a
commit 0f31a0fc76
3 changed files with 196 additions and 3 deletions

View File

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

View File

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

View File

@ -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://<host>:<port> (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
}