control/controlbase: make the protocol version number selectable.

This is so that we can plumb our client capability version through
the protocol as the Noise version. The capability version increments
more frequently than strictly required (the Noise version only needs
to change when cryptographically-significant changes are made to
the protocol, whereas the capability version also indicates changes
in non-cryptographically-significant parts of the protocol), but this
gives us a safe pre-auth way to determine if the client supports
future protocol features, while still relying on Noise's strong
assurance that the client and server have agreed on the same version.

Currently, the server executes the same protocol regardless of the
version number, and just presents the version to the caller so they
can do capability-based things in the upper RPC protocol. In future,
we may add a ratchet to disallow obsolete protocols, or vary the
Noise handshake behavior based on requested version.

Updates #3488

Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
David Anderson
2022-04-07 12:03:32 -07:00
committed by Dave Anderson
parent be861797b4
commit 02ad987e24
10 changed files with 98 additions and 44 deletions

View File

@@ -32,7 +32,7 @@ const (
protocolName = "Noise_IK_25519_ChaChaPoly_BLAKE2s"
// protocolVersion is the version of the control protocol that
// Client will use when initiating a handshake.
protocolVersion uint16 = 1
//protocolVersion uint16 = 1
// protocolVersionPrefix is the name portion of the protocol
// name+version string that gets mixed into the handshake as a
// prologue.
@@ -66,7 +66,7 @@ type HandshakeContinuation func(context.Context, net.Conn) (*Conn, error)
// protocol switching. By splitting the handshake into an initial
// message and a continuation, we can embed the handshake initiation
// into the HTTP protocol switching request and avoid a bit of delay.
func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic) (initialHandshake []byte, continueHandshake HandshakeContinuation, err error) {
func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (initialHandshake []byte, continueHandshake HandshakeContinuation, err error) {
var s symmetricState
s.Initialize()
@@ -78,7 +78,7 @@ func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic)
s.MixHash(controlKey.UntypedBytes())
// -> e, es, s, ss
init := mkInitiationMessage()
init := mkInitiationMessage(protocolVersion)
machineEphemeral := key.NewMachine()
machineEphemeralPub := machineEphemeral.Public()
copy(init.EphemeralPub(), machineEphemeralPub.UntypedBytes())
@@ -96,7 +96,7 @@ func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic)
s.EncryptAndHash(cipher, init.Tag(), nil) // empty message payload
cont := func(ctx context.Context, conn net.Conn) (*Conn, error) {
return continueClientHandshake(ctx, conn, &s, machineKey, machineEphemeral, controlKey)
return continueClientHandshake(ctx, conn, &s, machineKey, machineEphemeral, controlKey, protocolVersion)
}
return init[:], cont, nil
}
@@ -107,8 +107,8 @@ func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic)
// This is a helper for when you don't need the fancy
// continuation-style handshake, and just want to synchronously
// upgrade a net.Conn to a secure transport.
func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, controlKey key.MachinePublic) (*Conn, error) {
init, cont, err := ClientDeferred(machineKey, controlKey)
func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (*Conn, error) {
init, cont, err := ClientDeferred(machineKey, controlKey, protocolVersion)
if err != nil {
return nil, err
}
@@ -118,7 +118,7 @@ func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, c
return cont(ctx, conn)
}
func continueClientHandshake(ctx context.Context, conn net.Conn, s *symmetricState, machineKey, machineEphemeral key.MachinePrivate, controlKey key.MachinePublic) (*Conn, error) {
func continueClientHandshake(ctx context.Context, conn net.Conn, s *symmetricState, machineKey, machineEphemeral key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (*Conn, error) {
// No matter what, this function can only run once per s. Ensure
// attempted reuse causes a panic.
defer func() {
@@ -193,13 +193,19 @@ func continueClientHandshake(ctx context.Context, conn net.Conn, s *symmetricSta
// Server initiates a control server handshake, returning the resulting
// control connection.
//
// maxSupportedVersion is the highest handshake version the server is
// willing to handshake with. The server will handshake with any
// version from 0 to maxSupportedVersion inclusive, the caller should
// inspect conn.Version() to determine what version of the handshake
// was executed.
//
// optionalInit can be the client's initial handshake message as
// returned by ClientDeferred, or nil in which case the initial
// message is read from conn.
//
// The context deadline, if any, covers the entire handshaking
// process.
func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate, optionalInit []byte) (*Conn, error) {
func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate, maxSupportedVersion uint16, optionalInit []byte) (*Conn, error) {
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
return nil, fmt.Errorf("setting conn deadline: %w", err)
@@ -239,9 +245,16 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate, o
} else if _, err := io.ReadFull(conn, init.Header()); err != nil {
return nil, err
}
if init.Version() != protocolVersion {
return nil, sendErr("unsupported protocol version")
// Currently, these versions exclusively indicate what the upper
// RPC protocol understands, the Noise handshake is exactly the
// same in all versions. If that ever changes, this check will
// need to become more complex to handle different kinds of
// handshake.
if init.Version() > maxSupportedVersion {
return nil, sendErr("unsupported handshake version")
}
// Just a rename to make it more obvious what the value is
clientVersion := init.Version()
if init.Type() != msgTypeInitiation {
return nil, sendErr("unexpected handshake message type")
}
@@ -257,7 +270,7 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate, o
// prologue. Can only do this once we at least think the client is
// handshaking using a supported version.
s.MixHash(protocolVersionPrologue(protocolVersion))
s.MixHash(protocolVersionPrologue(clientVersion))
// <- s
// ...
@@ -310,7 +323,7 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate, o
c := &Conn{
conn: conn,
version: protocolVersion,
version: clientVersion,
peer: machineKey,
handshakeHash: s.h,
tx: txState{