mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-05 04:21:01 +02:00
Saves 352 KB, removing one of our two HTTP/2 implementations linked into the binary. Fixes #17305 Updates #15015 Change-Id: I53a04b1f2687dca73c8541949465038b69aa6ade Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
149 lines
4.8 KiB
Go
149 lines
4.8 KiB
Go
// 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"
|
|
"sync"
|
|
|
|
"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.
|
|
//
|
|
// Use [NewConn] to build a new Conn if you want [Conn.GetEarlyPayload] to work.
|
|
// Otherwise making a Conn directly, only setting Conn, is fine.
|
|
type Conn struct {
|
|
*controlbase.Conn
|
|
|
|
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
|
|
}
|
|
|
|
// NewConn 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 NewConn(conn *controlbase.Conn) *Conn {
|
|
return &Conn{
|
|
Conn: conn,
|
|
earlyPayloadReady: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
if c.earlyPayloadReady == nil {
|
|
return nil, errors.New("Conn was not created with NewConn; early payload not supported")
|
|
}
|
|
select {
|
|
case <-c.earlyPayloadReady:
|
|
return c.earlyPayload, c.earlyPayloadErr
|
|
default:
|
|
go c.readHeaderOnce.Do(c.readHeader)
|
|
}
|
|
select {
|
|
case <-c.earlyPayloadReady:
|
|
return c.earlyPayload, c.earlyPayloadErr
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
if c.earlyPayloadReady != nil {
|
|
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
|
|
}
|