Update
This commit is contained in:
312
vendor/tailscale.com/control/ts2021/client.go
generated
vendored
Normal file
312
vendor/tailscale.com/control/ts2021/client.go
generated
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ts2021
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
// Client provides a http.Client to connect to tailcontrol over
|
||||
// the ts2021 protocol.
|
||||
type Client struct {
|
||||
// Client is an HTTP client to talk to the coordination server.
|
||||
// It automatically makes a new Noise connection as needed.
|
||||
*http.Client
|
||||
|
||||
logf logger.Logf // non-nil
|
||||
opts ClientOpts
|
||||
host string // the host part of serverURL
|
||||
httpPort string // the default port to dial
|
||||
httpsPort string // the fallback Noise-over-https port or empty if none
|
||||
|
||||
// mu protects the following
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
connPool set.HandleSet[*Conn] // all live connections
|
||||
}
|
||||
|
||||
// ClientOpts contains options for the [NewClient] function. All fields are
|
||||
// required unless otherwise specified.
|
||||
type ClientOpts struct {
|
||||
// ServerURL is the URL of the server to connect to.
|
||||
ServerURL string
|
||||
|
||||
// PrivKey is this node's private key.
|
||||
PrivKey key.MachinePrivate
|
||||
|
||||
// ServerPubKey is the public key of the server.
|
||||
// It is of the form https://<host>:<port> (no trailing slash).
|
||||
ServerPubKey key.MachinePublic
|
||||
|
||||
// Dialer's SystemDial function is used to connect to the server.
|
||||
Dialer *tsdial.Dialer
|
||||
|
||||
// Optional fields follow
|
||||
|
||||
// Logf is the log function to use.
|
||||
// If nil, log.Printf is used.
|
||||
Logf logger.Logf
|
||||
|
||||
// NetMon is the network monitor that will be used to get the
|
||||
// network interface state. This field can be nil; if so, the current
|
||||
// state will be looked up dynamically.
|
||||
NetMon *netmon.Monitor
|
||||
|
||||
// DNSCache is the caching Resolver to use to connect to the server.
|
||||
//
|
||||
// This field can be nil.
|
||||
DNSCache *dnscache.Resolver
|
||||
|
||||
// HealthTracker, if non-nil, is the health tracker to use.
|
||||
HealthTracker *health.Tracker
|
||||
|
||||
// DialPlan, if set, is a function that should return an explicit plan
|
||||
// on how to connect to the server.
|
||||
DialPlan func() *tailcfg.ControlDialPlan
|
||||
|
||||
// ProtocolVersion, if non-zero, specifies an alternate
|
||||
// protocol version to use instead of the default,
|
||||
// of [tailcfg.CurrentCapabilityVersion].
|
||||
ProtocolVersion uint16
|
||||
}
|
||||
|
||||
// NewClient returns a new noiseClient for the provided server and machine key.
|
||||
//
|
||||
// netMon may be nil, if non-nil it's used to do faster interface lookups.
|
||||
// dialPlan may be nil
|
||||
func NewClient(opts ClientOpts) (*Client, error) {
|
||||
logf := opts.Logf
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
if opts.ServerURL == "" {
|
||||
return nil, errors.New("ServerURL is required")
|
||||
}
|
||||
if opts.PrivKey.IsZero() {
|
||||
return nil, errors.New("PrivKey is required")
|
||||
}
|
||||
if opts.ServerPubKey.IsZero() {
|
||||
return nil, errors.New("ServerPubKey is required")
|
||||
}
|
||||
if opts.Dialer == nil {
|
||||
return nil, errors.New("Dialer is required")
|
||||
}
|
||||
|
||||
u, err := url.Parse(opts.ServerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ClientOpts.ServerURL: %w", err)
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return nil, errors.New("invalid ServerURL scheme, must be http or https")
|
||||
}
|
||||
|
||||
httpPort, httpsPort := "80", "443"
|
||||
addr, _ := netip.ParseAddr(u.Hostname())
|
||||
isPrivateHost := addr.IsPrivate() || addr.IsLoopback() || u.Hostname() == "localhost"
|
||||
if port := u.Port(); port != "" {
|
||||
// If there is an explicit port specified, entirely rely on the scheme,
|
||||
// unless it's http with a private host in which case we never try using HTTPS.
|
||||
if u.Scheme == "https" {
|
||||
httpPort = ""
|
||||
httpsPort = port
|
||||
} else if u.Scheme == "http" {
|
||||
httpPort = port
|
||||
httpsPort = "443"
|
||||
if isPrivateHost {
|
||||
logf("setting empty HTTPS port with http scheme and private host %s", u.Hostname())
|
||||
httpsPort = ""
|
||||
}
|
||||
}
|
||||
} else if u.Scheme == "http" && isPrivateHost {
|
||||
// Whenever the scheme is http and the hostname is an IP address, do not set the HTTPS port,
|
||||
// as there cannot be a TLS certificate issued for an IP, unless it's a public IP.
|
||||
httpPort = "80"
|
||||
httpsPort = ""
|
||||
}
|
||||
|
||||
np := &Client{
|
||||
opts: opts,
|
||||
host: u.Hostname(),
|
||||
httpPort: httpPort,
|
||||
httpsPort: httpsPort,
|
||||
logf: logf,
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
Protocols: new(http.Protocols),
|
||||
MaxConnsPerHost: 1,
|
||||
}
|
||||
// We force only HTTP/2 for this transport, which is what the control server
|
||||
// speaks inside the ts2021 Noise encryption. But Go doesn't know about that,
|
||||
// so we use "SetUnencryptedHTTP2" even though it's actually encrypted.
|
||||
tr.Protocols.SetUnencryptedHTTP2(true)
|
||||
tr.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return np.dial(ctx)
|
||||
}
|
||||
|
||||
np.Client = &http.Client{Transport: tr}
|
||||
return np, nil
|
||||
}
|
||||
|
||||
// Close closes all the underlying noise connections.
|
||||
// It is a no-op and returns nil if the connection is already closed.
|
||||
func (nc *Client) Close() error {
|
||||
nc.mu.Lock()
|
||||
live := nc.connPool
|
||||
nc.closed = true
|
||||
nc.connPool = nil // stop noteConnClosed from mutating it as we loop over it (in live) below
|
||||
nc.mu.Unlock()
|
||||
|
||||
for _, c := range live {
|
||||
c.Close()
|
||||
}
|
||||
nc.Client.CloseIdleConnections()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dial opens a new connection to tailcontrol, fetching the server noise key
|
||||
// if not cached.
|
||||
func (nc *Client) dial(ctx context.Context) (*Conn, error) {
|
||||
if tailcfg.CurrentCapabilityVersion > math.MaxUint16 {
|
||||
// Panic, because a test should have started failing several
|
||||
// thousand version numbers before getting to this point.
|
||||
panic("capability version is too high to fit in the wire protocol")
|
||||
}
|
||||
|
||||
var dialPlan *tailcfg.ControlDialPlan
|
||||
if nc.opts.DialPlan != nil {
|
||||
dialPlan = nc.opts.DialPlan()
|
||||
}
|
||||
|
||||
// If we have a dial plan, then set our timeout as slightly longer than
|
||||
// the maximum amount of time contained therein; we assume that
|
||||
// explicit instructions on timeouts are more useful than a single
|
||||
// hard-coded timeout.
|
||||
//
|
||||
// The default value of 5 is chosen so that, when there's no dial plan,
|
||||
// we retain the previous behaviour of 10 seconds end-to-end timeout.
|
||||
timeoutSec := 5.0
|
||||
if dialPlan != nil {
|
||||
for _, c := range dialPlan.Candidates {
|
||||
if v := c.DialStartDelaySec + c.DialTimeoutSec; v > timeoutSec {
|
||||
timeoutSec = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After we establish a connection, we need some time to actually
|
||||
// upgrade it into a Noise connection. With a ballpark worst-case RTT
|
||||
// of 1000ms, give ourselves an extra 5 seconds to complete the
|
||||
// handshake.
|
||||
timeoutSec += 5
|
||||
|
||||
// Be extremely defensive and ensure that the timeout is in the range
|
||||
// [5, 60] seconds (e.g. if we accidentally get a negative number).
|
||||
if timeoutSec > 60 {
|
||||
timeoutSec = 60
|
||||
} else if timeoutSec < 5 {
|
||||
timeoutSec = 5
|
||||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec * float64(time.Second))
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
chd := &controlhttp.Dialer{
|
||||
Hostname: nc.host,
|
||||
HTTPPort: nc.httpPort,
|
||||
HTTPSPort: cmp.Or(nc.httpsPort, controlhttp.NoPort),
|
||||
MachineKey: nc.opts.PrivKey,
|
||||
ControlKey: nc.opts.ServerPubKey,
|
||||
ProtocolVersion: cmp.Or(nc.opts.ProtocolVersion, uint16(tailcfg.CurrentCapabilityVersion)),
|
||||
Dialer: nc.opts.Dialer.SystemDial,
|
||||
DNSCache: nc.opts.DNSCache,
|
||||
DialPlan: dialPlan,
|
||||
Logf: nc.logf,
|
||||
NetMon: nc.opts.NetMon,
|
||||
HealthTracker: nc.opts.HealthTracker,
|
||||
Clock: tstime.StdClock{},
|
||||
}
|
||||
clientConn, err := chd.Dial(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc.mu.Lock()
|
||||
|
||||
handle := set.NewHandle()
|
||||
ncc := NewConn(clientConn.Conn, func() { nc.noteConnClosed(handle) })
|
||||
mak.Set(&nc.connPool, handle, ncc)
|
||||
|
||||
if nc.closed {
|
||||
nc.mu.Unlock()
|
||||
ncc.Close() // Needs to be called without holding the lock.
|
||||
return nil, errors.New("noise client closed")
|
||||
}
|
||||
|
||||
defer nc.mu.Unlock()
|
||||
return ncc, nil
|
||||
}
|
||||
|
||||
// noteConnClosed notes that the *Conn with the given handle has closed and
|
||||
// should be removed from the live connPool (which is usually of size 0 or 1,
|
||||
// except perhaps briefly 2 during a network failure and reconnect).
|
||||
func (nc *Client) noteConnClosed(handle set.Handle) {
|
||||
nc.mu.Lock()
|
||||
defer nc.mu.Unlock()
|
||||
nc.connPool.Delete(handle)
|
||||
}
|
||||
|
||||
// post does a POST to the control server at the given path, JSON-encoding body.
|
||||
// The provided nodeKey is an optional load balancing hint.
|
||||
func (nc *Client) Post(ctx context.Context, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
|
||||
return nc.DoWithBody(ctx, "POST", path, nodeKey, body)
|
||||
}
|
||||
|
||||
func (nc *Client) DoWithBody(ctx context.Context, method, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
|
||||
jbody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, "https://"+nc.host+path, bytes.NewReader(jbody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
AddLBHeader(req, nodeKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return nc.Do(req)
|
||||
}
|
||||
|
||||
// AddLBHeader adds the load balancer header to req if nodeKey is non-zero.
|
||||
func AddLBHeader(req *http.Request, nodeKey key.NodePublic) {
|
||||
if !nodeKey.IsZero() {
|
||||
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user