control/ts2021: rename from internal/noiseconn in prep for controlclient split
A following change will split out the controlclient.NoiseClient type out, away from the rest of the controlclient package which is relatively dependency heavy. A question was where to move it, and whether to make a new (a fifth!) package in the ts2021 dependency chain. @creachadair and I brainstormed and decided to merge internal/noiseconn and controlclient.NoiseClient into one package, with names ts2021.Conn and ts2021.Client. For ease of reviewing the subsequent PR, this is the first step that just renames the internal/noiseconn package to control/ts2021. Updates #17305 Change-Id: Ib5ea162dc1d336c1d805bdd9548d1702dd6e1468 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
801aac59db
commit
78af49dd1a
@@ -18,8 +18,8 @@ import (
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/control/ts2021"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/internal/noiseconn"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
@@ -50,7 +50,7 @@ type NoiseClient struct {
|
||||
|
||||
// sfDial ensures that two concurrent requests for a noise connection only
|
||||
// produce one shared one between the two callers.
|
||||
sfDial singleflight.Group[struct{}, *noiseconn.Conn]
|
||||
sfDial singleflight.Group[struct{}, *ts2021.Conn]
|
||||
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
@@ -72,9 +72,9 @@ type NoiseClient struct {
|
||||
// mu only protects the following variables.
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
last *noiseconn.Conn // or nil
|
||||
last *ts2021.Conn // or nil
|
||||
nextID int
|
||||
connPool map[int]*noiseconn.Conn // active connections not yet closed; see noiseconn.Conn.Close
|
||||
connPool map[int]*ts2021.Conn // active connections not yet closed; see ts2021.Conn.Close
|
||||
}
|
||||
|
||||
// NoiseOpts contains options for the NewNoiseClient function. All fields are
|
||||
@@ -195,12 +195,12 @@ func (e contextErr) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
// getConn returns a noiseconn.Conn that can be used to make requests to the
|
||||
// getConn returns a ts2021.Conn that can be used to make requests to the
|
||||
// coordination server. It may return a cached connection or create a new one.
|
||||
// Dials are singleflighted, so concurrent calls to getConn may only dial once.
|
||||
// As such, context values may not be respected as there are no guarantees that
|
||||
// the context passed to getConn is the same as the context passed to dial.
|
||||
func (nc *NoiseClient) getConn(ctx context.Context) (*noiseconn.Conn, error) {
|
||||
func (nc *NoiseClient) getConn(ctx context.Context) (*ts2021.Conn, error) {
|
||||
nc.mu.Lock()
|
||||
if last := nc.last; last != nil && last.CanTakeNewRequest() {
|
||||
nc.mu.Unlock()
|
||||
@@ -214,7 +214,7 @@ func (nc *NoiseClient) getConn(ctx context.Context) (*noiseconn.Conn, error) {
|
||||
// canceled. Instead, we have to additionally check that the context
|
||||
// which was canceled is our context and retry if our context is still
|
||||
// valid.
|
||||
conn, err, _ := nc.sfDial.Do(struct{}{}, func() (*noiseconn.Conn, error) {
|
||||
conn, err, _ := nc.sfDial.Do(struct{}{}, func() (*ts2021.Conn, error) {
|
||||
c, err := nc.dial(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
@@ -282,7 +282,7 @@ func (nc *NoiseClient) Close() error {
|
||||
|
||||
// dial opens a new connection to tailcontrol, fetching the server noise key
|
||||
// if not cached.
|
||||
func (nc *NoiseClient) dial(ctx context.Context) (*noiseconn.Conn, error) {
|
||||
func (nc *NoiseClient) dial(ctx context.Context) (*ts2021.Conn, error) {
|
||||
nc.mu.Lock()
|
||||
connID := nc.nextID
|
||||
nc.nextID++
|
||||
@@ -352,7 +352,7 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseconn.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ncc, err := noiseconn.New(clientConn.Conn, nc.h2t, connID, nc.connClosed)
|
||||
ncc, err := ts2021.New(clientConn.Conn, nc.h2t, connID, nc.connClosed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/control/controlhttp/controlhttpserver"
|
||||
"tailscale.com/internal/noiseconn"
|
||||
"tailscale.com/control/ts2021"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -310,7 +310,7 @@ func (up *Upgrader) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// https://httpwg.org/specs/rfc7540.html#rfc.section.4.1 (Especially not
|
||||
// an HTTP/2 settings frame, which isn't of type 'T')
|
||||
var notH2Frame [5]byte
|
||||
copy(notH2Frame[:], noiseconn.EarlyPayloadMagic)
|
||||
copy(notH2Frame[:], ts2021.EarlyPayloadMagic)
|
||||
var lenBuf [4]byte
|
||||
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(earlyJSON)))
|
||||
// These writes are all buffered by caller, so fine to do them
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ts2021 handles the details of the Tailscale 2021 control protocol
|
||||
// that are after (above) the Noise layer. In particular, the
|
||||
// "tailcfg.EarlyNoise" message and the subsequent HTTP/2 connection.
|
||||
package ts2021
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// Conn 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, properly handles an optional "early
|
||||
// payload" that's sent prior to beginning the HTTP/2 session, and provides a
|
||||
// way to return a connection to a pool when the connection is closed.
|
||||
type Conn struct {
|
||||
*controlbase.Conn
|
||||
id int
|
||||
onClose func(int)
|
||||
h2cc *http2.ClientConn
|
||||
|
||||
readHeaderOnce sync.Once // guards init of reader field
|
||||
reader io.Reader // (effectively Conn.Reader after header)
|
||||
earlyPayloadReady chan struct{} // closed after earlyPayload is set (including set to nil)
|
||||
earlyPayload *tailcfg.EarlyNoise
|
||||
earlyPayloadErr error
|
||||
}
|
||||
|
||||
// New creates a new Conn that wraps the given controlbase.Conn.
|
||||
//
|
||||
// h2t is the HTTP/2 transport to use for the connection; a new
|
||||
// http2.ClientConn will be created that reads from the returned Conn.
|
||||
//
|
||||
// connID should be a unique ID for this connection. When the Conn is closed,
|
||||
// the onClose function will be called with the connID if it is non-nil.
|
||||
func New(conn *controlbase.Conn, h2t *http2.Transport, connID int, onClose func(int)) (*Conn, error) {
|
||||
ncc := &Conn{
|
||||
Conn: conn,
|
||||
id: connID,
|
||||
onClose: onClose,
|
||||
earlyPayloadReady: make(chan struct{}),
|
||||
}
|
||||
h2cc, err := h2t.NewClientConn(ncc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ncc.h2cc = h2cc
|
||||
return ncc, nil
|
||||
}
|
||||
|
||||
// RoundTrip implements the http.RoundTripper interface.
|
||||
func (c *Conn) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return c.h2cc.RoundTrip(r)
|
||||
}
|
||||
|
||||
// GetEarlyPayload waits for the early Noise payload to arrive.
|
||||
// It may return (nil, nil) if the server begins HTTP/2 without one.
|
||||
//
|
||||
// It is safe to call this multiple times; all callers will block until the
|
||||
// early Noise payload is ready (if any) and will return the same result for
|
||||
// the lifetime of the Conn.
|
||||
func (c *Conn) GetEarlyPayload(ctx context.Context) (*tailcfg.EarlyNoise, error) {
|
||||
select {
|
||||
case <-c.earlyPayloadReady:
|
||||
return c.earlyPayload, c.earlyPayloadErr
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// CanTakeNewRequest reports whether the underlying HTTP/2 connection can take
|
||||
// a new request, meaning it has not been closed or received or sent a GOAWAY.
|
||||
func (c *Conn) CanTakeNewRequest() bool {
|
||||
return c.h2cc.CanTakeNewRequest()
|
||||
}
|
||||
|
||||
// The first 9 bytes from the server to client over Noise are either an HTTP/2
|
||||
// settings frame (a normal HTTP/2 setup) or, as we added later, an "early payload"
|
||||
// header that's also 9 bytes long: 5 bytes (EarlyPayloadMagic) followed by 4 bytes
|
||||
// of length. Then that many bytes of JSON-encoded tailcfg.EarlyNoise.
|
||||
// The early payload is optional. Some servers may not send it.
|
||||
const (
|
||||
hdrLen = 9 // http2 frame header size; also size of our early payload size header
|
||||
)
|
||||
|
||||
// EarlyPayloadMagic is the 5-byte magic prefix that indicates an early payload.
|
||||
const EarlyPayloadMagic = "\xff\xff\xffTS"
|
||||
|
||||
// returnErrReader is an io.Reader that always returns an error.
|
||||
type returnErrReader struct {
|
||||
err error // the error to return
|
||||
}
|
||||
|
||||
func (r returnErrReader) Read([]byte) (int, error) { return 0, r.err }
|
||||
|
||||
// Read is basically the same as controlbase.Conn.Read, but it first reads the
|
||||
// "early payload" header from the server which may or may not be present,
|
||||
// depending on the server.
|
||||
func (c *Conn) Read(p []byte) (n int, err error) {
|
||||
c.readHeaderOnce.Do(c.readHeader)
|
||||
return c.reader.Read(p)
|
||||
}
|
||||
|
||||
// readHeader reads the optional "early payload" from the server that arrives
|
||||
// after the Noise handshake but before the HTTP/2 session begins.
|
||||
//
|
||||
// readHeader is responsible for reading the header (if present), initializing
|
||||
// c.earlyPayload, closing c.earlyPayloadReady, and initializing c.reader for
|
||||
// future reads.
|
||||
func (c *Conn) readHeader() {
|
||||
defer close(c.earlyPayloadReady)
|
||||
|
||||
setErr := func(err error) {
|
||||
c.reader = returnErrReader{err}
|
||||
c.earlyPayloadErr = err
|
||||
}
|
||||
|
||||
var hdr [hdrLen]byte
|
||||
if _, err := io.ReadFull(c.Conn, hdr[:]); err != nil {
|
||||
setErr(err)
|
||||
return
|
||||
}
|
||||
if string(hdr[:len(EarlyPayloadMagic)]) != EarlyPayloadMagic {
|
||||
// No early payload. We have to return the 9 bytes read we already
|
||||
// consumed.
|
||||
c.reader = io.MultiReader(bytes.NewReader(hdr[:]), c.Conn)
|
||||
return
|
||||
}
|
||||
epLen := binary.BigEndian.Uint32(hdr[len(EarlyPayloadMagic):])
|
||||
if epLen > 10<<20 {
|
||||
setErr(errors.New("invalid early payload length"))
|
||||
return
|
||||
}
|
||||
payBuf := make([]byte, epLen)
|
||||
if _, err := io.ReadFull(c.Conn, payBuf); err != nil {
|
||||
setErr(err)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(payBuf, &c.earlyPayload); err != nil {
|
||||
setErr(err)
|
||||
return
|
||||
}
|
||||
c.reader = c.Conn
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c *Conn) Close() error {
|
||||
if err := c.Conn.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.onClose != nil {
|
||||
c.onClose(c.id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user