mirror of
https://github.com/tailscale/tailscale.git
synced 2026-01-24 07:38:40 -05:00
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
313 lines
9.2 KiB
Go
313 lines
9.2 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// 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())
|
|
}
|
|
}
|