client.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package ts2021
  4. import (
  5. "bytes"
  6. "cmp"
  7. "context"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "log"
  12. "math"
  13. "net"
  14. "net/http"
  15. "net/netip"
  16. "net/url"
  17. "sync"
  18. "time"
  19. "tailscale.com/control/controlhttp"
  20. "tailscale.com/health"
  21. "tailscale.com/net/dnscache"
  22. "tailscale.com/net/netmon"
  23. "tailscale.com/net/tsdial"
  24. "tailscale.com/tailcfg"
  25. "tailscale.com/tstime"
  26. "tailscale.com/types/key"
  27. "tailscale.com/types/logger"
  28. "tailscale.com/util/mak"
  29. "tailscale.com/util/set"
  30. )
  31. // Client provides a http.Client to connect to tailcontrol over
  32. // the ts2021 protocol.
  33. type Client struct {
  34. // Client is an HTTP client to talk to the coordination server.
  35. // It automatically makes a new Noise connection as needed.
  36. *http.Client
  37. logf logger.Logf // non-nil
  38. opts ClientOpts
  39. host string // the host part of serverURL
  40. httpPort string // the default port to dial
  41. httpsPort string // the fallback Noise-over-https port or empty if none
  42. // mu protects the following
  43. mu sync.Mutex
  44. closed bool
  45. connPool set.HandleSet[*Conn] // all live connections
  46. }
  47. // ClientOpts contains options for the [NewClient] function. All fields are
  48. // required unless otherwise specified.
  49. type ClientOpts struct {
  50. // ServerURL is the URL of the server to connect to.
  51. ServerURL string
  52. // PrivKey is this node's private key.
  53. PrivKey key.MachinePrivate
  54. // ServerPubKey is the public key of the server.
  55. // It is of the form https://<host>:<port> (no trailing slash).
  56. ServerPubKey key.MachinePublic
  57. // Dialer's SystemDial function is used to connect to the server.
  58. Dialer *tsdial.Dialer
  59. // Optional fields follow
  60. // Logf is the log function to use.
  61. // If nil, log.Printf is used.
  62. Logf logger.Logf
  63. // NetMon is the network monitor that will be used to get the
  64. // network interface state. This field can be nil; if so, the current
  65. // state will be looked up dynamically.
  66. NetMon *netmon.Monitor
  67. // DNSCache is the caching Resolver to use to connect to the server.
  68. //
  69. // This field can be nil.
  70. DNSCache *dnscache.Resolver
  71. // HealthTracker, if non-nil, is the health tracker to use.
  72. HealthTracker *health.Tracker
  73. // DialPlan, if set, is a function that should return an explicit plan
  74. // on how to connect to the server.
  75. DialPlan func() *tailcfg.ControlDialPlan
  76. // ProtocolVersion, if non-zero, specifies an alternate
  77. // protocol version to use instead of the default,
  78. // of [tailcfg.CurrentCapabilityVersion].
  79. ProtocolVersion uint16
  80. }
  81. // NewClient returns a new noiseClient for the provided server and machine key.
  82. //
  83. // netMon may be nil, if non-nil it's used to do faster interface lookups.
  84. // dialPlan may be nil
  85. func NewClient(opts ClientOpts) (*Client, error) {
  86. logf := opts.Logf
  87. if logf == nil {
  88. logf = log.Printf
  89. }
  90. if opts.ServerURL == "" {
  91. return nil, errors.New("ServerURL is required")
  92. }
  93. if opts.PrivKey.IsZero() {
  94. return nil, errors.New("PrivKey is required")
  95. }
  96. if opts.ServerPubKey.IsZero() {
  97. return nil, errors.New("ServerPubKey is required")
  98. }
  99. if opts.Dialer == nil {
  100. return nil, errors.New("Dialer is required")
  101. }
  102. u, err := url.Parse(opts.ServerURL)
  103. if err != nil {
  104. return nil, fmt.Errorf("invalid ClientOpts.ServerURL: %w", err)
  105. }
  106. if u.Scheme != "http" && u.Scheme != "https" {
  107. return nil, errors.New("invalid ServerURL scheme, must be http or https")
  108. }
  109. httpPort, httpsPort := "80", "443"
  110. addr, _ := netip.ParseAddr(u.Hostname())
  111. isPrivateHost := addr.IsPrivate() || addr.IsLoopback() || u.Hostname() == "localhost"
  112. if port := u.Port(); port != "" {
  113. // If there is an explicit port specified, entirely rely on the scheme,
  114. // unless it's http with a private host in which case we never try using HTTPS.
  115. if u.Scheme == "https" {
  116. httpPort = ""
  117. httpsPort = port
  118. } else if u.Scheme == "http" {
  119. httpPort = port
  120. httpsPort = "443"
  121. if isPrivateHost {
  122. logf("setting empty HTTPS port with http scheme and private host %s", u.Hostname())
  123. httpsPort = ""
  124. }
  125. }
  126. } else if u.Scheme == "http" && isPrivateHost {
  127. // Whenever the scheme is http and the hostname is an IP address, do not set the HTTPS port,
  128. // as there cannot be a TLS certificate issued for an IP, unless it's a public IP.
  129. httpPort = "80"
  130. httpsPort = ""
  131. }
  132. np := &Client{
  133. opts: opts,
  134. host: u.Hostname(),
  135. httpPort: httpPort,
  136. httpsPort: httpsPort,
  137. logf: logf,
  138. }
  139. tr := &http.Transport{
  140. Protocols: new(http.Protocols),
  141. MaxConnsPerHost: 1,
  142. }
  143. // We force only HTTP/2 for this transport, which is what the control server
  144. // speaks inside the ts2021 Noise encryption. But Go doesn't know about that,
  145. // so we use "SetUnencryptedHTTP2" even though it's actually encrypted.
  146. tr.Protocols.SetUnencryptedHTTP2(true)
  147. tr.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
  148. return np.dial(ctx)
  149. }
  150. np.Client = &http.Client{Transport: tr}
  151. return np, nil
  152. }
  153. // Close closes all the underlying noise connections.
  154. // It is a no-op and returns nil if the connection is already closed.
  155. func (nc *Client) Close() error {
  156. nc.mu.Lock()
  157. live := nc.connPool
  158. nc.closed = true
  159. nc.connPool = nil // stop noteConnClosed from mutating it as we loop over it (in live) below
  160. nc.mu.Unlock()
  161. for _, c := range live {
  162. c.Close()
  163. }
  164. nc.Client.CloseIdleConnections()
  165. return nil
  166. }
  167. // dial opens a new connection to tailcontrol, fetching the server noise key
  168. // if not cached.
  169. func (nc *Client) dial(ctx context.Context) (*Conn, error) {
  170. if tailcfg.CurrentCapabilityVersion > math.MaxUint16 {
  171. // Panic, because a test should have started failing several
  172. // thousand version numbers before getting to this point.
  173. panic("capability version is too high to fit in the wire protocol")
  174. }
  175. var dialPlan *tailcfg.ControlDialPlan
  176. if nc.opts.DialPlan != nil {
  177. dialPlan = nc.opts.DialPlan()
  178. }
  179. // If we have a dial plan, then set our timeout as slightly longer than
  180. // the maximum amount of time contained therein; we assume that
  181. // explicit instructions on timeouts are more useful than a single
  182. // hard-coded timeout.
  183. //
  184. // The default value of 5 is chosen so that, when there's no dial plan,
  185. // we retain the previous behaviour of 10 seconds end-to-end timeout.
  186. timeoutSec := 5.0
  187. if dialPlan != nil {
  188. for _, c := range dialPlan.Candidates {
  189. if v := c.DialStartDelaySec + c.DialTimeoutSec; v > timeoutSec {
  190. timeoutSec = v
  191. }
  192. }
  193. }
  194. // After we establish a connection, we need some time to actually
  195. // upgrade it into a Noise connection. With a ballpark worst-case RTT
  196. // of 1000ms, give ourselves an extra 5 seconds to complete the
  197. // handshake.
  198. timeoutSec += 5
  199. // Be extremely defensive and ensure that the timeout is in the range
  200. // [5, 60] seconds (e.g. if we accidentally get a negative number).
  201. if timeoutSec > 60 {
  202. timeoutSec = 60
  203. } else if timeoutSec < 5 {
  204. timeoutSec = 5
  205. }
  206. timeout := time.Duration(timeoutSec * float64(time.Second))
  207. ctx, cancel := context.WithTimeout(ctx, timeout)
  208. defer cancel()
  209. chd := &controlhttp.Dialer{
  210. Hostname: nc.host,
  211. HTTPPort: nc.httpPort,
  212. HTTPSPort: cmp.Or(nc.httpsPort, controlhttp.NoPort),
  213. MachineKey: nc.opts.PrivKey,
  214. ControlKey: nc.opts.ServerPubKey,
  215. ProtocolVersion: cmp.Or(nc.opts.ProtocolVersion, uint16(tailcfg.CurrentCapabilityVersion)),
  216. Dialer: nc.opts.Dialer.SystemDial,
  217. DNSCache: nc.opts.DNSCache,
  218. DialPlan: dialPlan,
  219. Logf: nc.logf,
  220. NetMon: nc.opts.NetMon,
  221. HealthTracker: nc.opts.HealthTracker,
  222. Clock: tstime.StdClock{},
  223. }
  224. clientConn, err := chd.Dial(ctx)
  225. if err != nil {
  226. return nil, err
  227. }
  228. nc.mu.Lock()
  229. handle := set.NewHandle()
  230. ncc := NewConn(clientConn.Conn, func() { nc.noteConnClosed(handle) })
  231. mak.Set(&nc.connPool, handle, ncc)
  232. if nc.closed {
  233. nc.mu.Unlock()
  234. ncc.Close() // Needs to be called without holding the lock.
  235. return nil, errors.New("noise client closed")
  236. }
  237. defer nc.mu.Unlock()
  238. return ncc, nil
  239. }
  240. // noteConnClosed notes that the *Conn with the given handle has closed and
  241. // should be removed from the live connPool (which is usually of size 0 or 1,
  242. // except perhaps briefly 2 during a network failure and reconnect).
  243. func (nc *Client) noteConnClosed(handle set.Handle) {
  244. nc.mu.Lock()
  245. defer nc.mu.Unlock()
  246. nc.connPool.Delete(handle)
  247. }
  248. // post does a POST to the control server at the given path, JSON-encoding body.
  249. // The provided nodeKey is an optional load balancing hint.
  250. func (nc *Client) Post(ctx context.Context, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
  251. return nc.DoWithBody(ctx, "POST", path, nodeKey, body)
  252. }
  253. func (nc *Client) DoWithBody(ctx context.Context, method, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
  254. jbody, err := json.Marshal(body)
  255. if err != nil {
  256. return nil, err
  257. }
  258. req, err := http.NewRequestWithContext(ctx, method, "https://"+nc.host+path, bytes.NewReader(jbody))
  259. if err != nil {
  260. return nil, err
  261. }
  262. AddLBHeader(req, nodeKey)
  263. req.Header.Set("Content-Type", "application/json")
  264. return nc.Do(req)
  265. }
  266. // AddLBHeader adds the load balancer header to req if nodeKey is non-zero.
  267. func AddLBHeader(req *http.Request, nodeKey key.NodePublic) {
  268. if !nodeKey.IsZero() {
  269. req.Header.Add(tailcfg.LBHeader, nodeKey.String())
  270. }
  271. }