control/controlclient, tailcfg: add support for EarlyNoise payload before http/2

Not yet used, but skipped over, parsed, and tested.

Updates #5972

Change-Id: Icd00196959ce266ae16a6c9244bd5e458e2c2947
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2022-10-30 08:42:54 -07:00
committed by Brad Fitzpatrick
parent a7f7e79245
commit 988c1f0ac7
3 changed files with 164 additions and 24 deletions

View File

@ -7,7 +7,10 @@ package controlclient
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"io"
"math"
"net/http"
"net/url"
@ -34,6 +37,77 @@ type noiseConn struct {
id int
pool *noiseClient
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
}
func (c *noiseConn) RoundTrip(r *http.Request) (*http.Response, error) {
return c.h2cc.RoundTrip(r)
}
// 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 = "\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 *noiseConn) 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 *noiseConn) readHeader() {
var hdr [hdrLen]byte
if _, err := io.ReadFull(c.Conn, hdr[:]); err != nil {
c.reader = returnErrReader{err}
return
}
if string(hdr[:len(earlyPayloadMagic)]) != earlyPayloadMagic {
// No early payload. We have to return the 9 bytes read we already
// consumed.
close(c.earlyPayloadReady)
c.reader = io.MultiReader(bytes.NewReader(hdr[:]), c.Conn)
return
}
epLen := binary.BigEndian.Uint32(hdr[len(earlyPayloadMagic):])
if epLen > 10<<20 {
c.reader = returnErrReader{errors.New("invalid early payload length")}
return
}
payBuf := make([]byte, epLen)
if _, err := io.ReadFull(c.Conn, payBuf); err != nil {
c.reader = returnErrReader{err}
return
}
if err := json.Unmarshal(payBuf, &c.earlyPayload); err != nil {
c.reader = returnErrReader{err}
return
}
close(c.earlyPayloadReady)
c.reader = c.Conn
}
func (c *noiseConn) Close() error {
@ -88,7 +162,7 @@ type noiseClient struct {
// serverURL is of the form https://<host>:<port> (no trailing slash).
//
// dialPlan may be nil
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*noiseClient, error) {
func newNoiseClient(privKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*noiseClient, error) {
u, err := url.Parse(serverURL)
if err != nil {
return nil, err
@ -111,7 +185,7 @@ func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, s
}
np := &noiseClient{
serverPubKey: serverPubKey,
privKey: priKey,
privKey: privKey,
host: u.Hostname(),
httpPort: httpPort,
httpsPort: httpsPort,
@ -157,7 +231,7 @@ func (nc *noiseClient) RoundTrip(req *http.Request) (*http.Response, error) {
if err != nil {
return nil, err
}
return conn.h2cc.RoundTrip(req)
return conn.RoundTrip(req)
}
// connClosed removes the connection with the provided ID from the pool
@ -259,14 +333,12 @@ func (nc *noiseClient) dial() (*noiseConn, error) {
}
ncc := &noiseConn{
Conn: clientConn.Conn,
id: connID,
pool: nc,
Conn: clientConn.Conn,
id: connID,
pool: nc,
earlyPayloadReady: make(chan struct{}),
}
// TODO(bradfitz): wrap clientConn in a type that sniffs the leading bytes
// from the server to see if it has early post-Noise, pre-H2 data for us.
h2cc, err := nc.h2t.NewClientConn(ncc)
if err != nil {
return nil, err