Files
coder/vpn/speaker.go
T
Spike Curtis 40802958e9 fix: use explicit api versions for agent and tailnet (#15508)
Bumps the Tailnet and Agent API version 2.3, and creates some extra controls and machinery around these versions.

What happened is that we accidentally shipped two new API features without bumping the version.  `ScriptCompleted` on the Agent API in Coder v2.16 and `RefreshResumeToken` on the Tailnet API in Coder v2.15.

Since we can't easily retroactively bump the versions, we'll roll these changes into API version 2.3 along with the new WorkspaceUpdates RPC, which hasn't been released yet.  That means there is some ambiguity in Coder v2.15-v2.17 about exactly what methods are supported on the Tailnet and Agent APIs.  This isn't great, but hasn't caused us major issues because 

1. RefreshResumeToken is considered optional, and clients just log and move on if the RPC isn't supported. 
2. Agents basically never get started talking to a Coderd that is older than they are, since the agent binary is normally downloaded from Coderd at workspace start.

Still it's good to get things squared away in terms of versions for SDK users and possible edge cases around client and server versions.

To mitigate against this thing happening again, this PR also:

1. adds a CODEOWNERS for the API proto packages, so I'll review changes
2. defines interface types for different API versions, and has the agent explicitly use a specific version.  That way, if you add a new method, and try to use it in the agent without thinking explicitly about versions, it won't compile.

With the protocol controllers stuff, we've sort of already abstracted the Tailnet API such that the interface type strategy won't work, but I'll work on getting the Controller to be version aware, such that it can check the API version it's getting against the controllers it has -- in a later PR.
2024-11-15 11:16:28 +04:00

369 lines
12 KiB
Go

package vpn
import (
"context"
"fmt"
"io"
"strings"
"sync"
"golang.org/x/xerrors"
"google.golang.org/protobuf/proto"
"cdr.dev/slog"
"github.com/coder/coder/v2/apiversion"
)
type SpeakerRole string
type rpcMessage interface {
proto.Message
GetRpc() *RPC
// EnsureRPC isn't autogenerated, but we'll manually add it for RPC types so that the speaker
// can allocate the RPC.
EnsureRPC() *RPC
}
func (t *TunnelMessage) EnsureRPC() *RPC {
if t.Rpc == nil {
t.Rpc = &RPC{}
}
return t.Rpc
}
func (m *ManagerMessage) EnsureRPC() *RPC {
if m.Rpc == nil {
m.Rpc = &RPC{}
}
return m.Rpc
}
// receivableRPCMessage is an rpcMessage that we can receive, and unmarshal, using generics, from a
// byte stream. proto.Unmarshal requires us to have already allocated the memory for the message
// type we are unmarshalling. All our message types are pointers like *TunnelMessage, so to
// allocate, the compiler needs to know:
//
// a) that the type is a pointer type
// b) what type it is pointing to
//
// So, this generic interface requires that the message is a pointer to the type RR. Then, we pass
// both the receivableRPCMessage and RR as type constraints, so that we'll have access to the
// underlying type when it comes time to allocate it. It's a bit messy, but the alternative is
// reflection, which has its own challenges in understandability.
type receivableRPCMessage[RR any] interface {
rpcMessage
*RR
}
const (
SpeakerRoleManager SpeakerRole = "manager"
SpeakerRoleTunnel SpeakerRole = "tunnel"
)
// speaker is an implementation of the CoderVPN protocol. It handles unary RPCs and their responses,
// as well as the low-level serialization & deserialization to the ReadWriteCloser (rwc).
//
// ┌────────┐ sendCh
// ◄─────│ ◄────────────────────────────────────────────────────────────────── ◄┐
// │ │ ▲ rpc requests
// rwc │ serdes │ │ │ sendReply()
// │ │ ┌───────────────────┐ ┌──────┼──────┐
// ──────► ┼────────► recvFromSerdes() │ rpc │rpc handling │ │
// └────────┘ recvCh │ ┼────────────► ◄──── unaryRPC()
// │ │ responses │ │ │
// │ │ │ │
// │ │ └─────────────┘ ┌ ─ ─│─ ─ ─ ─ ─ ─ ─ ┐
// │ ┼──────────────────────────────────────────► request handling
// └───────────────────┘ requests (outside speaker)
// └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
//
// speaker is implemented as a generic type that accepts the type of message we send (S), the type we receive (R), and
// the underlying type that R points to (RR). The speaker is intended to be wrapped by another, non-generic type for
// the role (manager or tunnel). E.g. Tunnel from this package.
//
// The serdes handles SERialiazation and DESerialization of the low level message types. The wrapping type may send
// non-RPC messages (that is messages that don't expect an explicit reply) by sending on the sendCh.
//
// Unary RPCs are handled by the unaryRPC() function, which handles sending the message and waiting for the response.
//
// recvFromSerdes() reads all incoming messages from the serdes. If they are RPC responses, it dispatches them to the
// waiting unaryRPC() function call, if any. If they are RPC requests or non-RPC messages, it wraps them in a request
// struct and sends them over the requests chan. The manager/tunnel role type must read from this chan and handle
// the requests. If they are RPC types, it should call sendReply() on the request with the reply message.
type speaker[S rpcMessage, R receivableRPCMessage[RR], RR any] struct {
serdes *serdes[S, R, RR]
requests chan *request[S, R]
logger slog.Logger
nextMsgID uint64
ctx context.Context
cancel context.CancelFunc
sendCh chan<- S
recvCh <-chan R
recvLoopDone chan struct{}
mu sync.Mutex
responseChans map[uint64]chan R
}
// newSpeaker creates a new protocol speaker.
func newSpeaker[S rpcMessage, R receivableRPCMessage[RR], RR any](
ctx context.Context, logger slog.Logger, conn io.ReadWriteCloser,
me, them SpeakerRole,
) (
*speaker[S, R, RR], error,
) {
ctx, cancel := context.WithCancel(ctx)
if err := handshake(ctx, conn, logger, me, them); err != nil {
cancel()
return nil, xerrors.Errorf("handshake failed: %w", err)
}
sendCh := make(chan S)
recvCh := make(chan R)
s := &speaker[S, R, RR]{
serdes: newSerdes(ctx, logger, conn, sendCh, recvCh),
logger: logger,
requests: make(chan *request[S, R]),
responseChans: make(map[uint64]chan R),
nextMsgID: 1,
ctx: ctx,
cancel: cancel,
sendCh: sendCh,
recvCh: recvCh,
recvLoopDone: make(chan struct{}),
}
return s, nil
}
// start starts the serialzation/deserialization. It's important this happens
// after any assignments of the speaker to its owning Tunnel or Manager, since
// the mutex is copied and that is not threadsafe.
// nolint: revive
func (s *speaker[_, _, _]) start() {
s.serdes.start()
go s.recvFromSerdes()
}
func (s *speaker[S, R, _]) recvFromSerdes() {
defer close(s.recvLoopDone)
defer close(s.requests)
for {
select {
case <-s.ctx.Done():
s.logger.Debug(s.ctx, "recvFromSerdes context done while waiting for proto", slog.Error(s.ctx.Err()))
return
case msg, ok := <-s.recvCh:
if !ok {
s.logger.Debug(s.ctx, "recvCh is closed")
return
}
rpc := msg.GetRpc()
if rpc != nil && rpc.ResponseTo != 0 {
// this is a unary response
s.tryToDeliverResponse(msg)
continue
}
req := &request[S, R]{
ctx: s.ctx,
msg: msg,
replyCh: s.sendCh,
}
select {
case <-s.ctx.Done():
s.logger.Debug(s.ctx, "recvFromSerdes context done while waiting for request handler", slog.Error(s.ctx.Err()))
return
case s.requests <- req:
}
}
}
}
// Close closes the speaker
// nolint: revive
func (s *speaker[_, _, _]) Close() error {
s.cancel()
err := s.serdes.Close()
return err
}
// unaryRPC sends a request/response style RPC over the protocol, waits for the response, then
// returns the response
func (s *speaker[S, R, _]) unaryRPC(ctx context.Context, req S) (resp R, err error) {
rpc := req.EnsureRPC()
msgID, respCh := s.newRPC()
rpc.MsgId = msgID
logger := s.logger.With(slog.F("msg_id", msgID))
select {
case <-ctx.Done():
return resp, ctx.Err()
case <-s.ctx.Done():
return resp, xerrors.Errorf("vpn protocol closed: %w", s.ctx.Err())
case <-s.recvLoopDone:
logger.Debug(s.ctx, "recvLoopDone while sending request")
return resp, io.ErrUnexpectedEOF
case s.sendCh <- req:
logger.Debug(s.ctx, "sent rpc request", slog.F("req", req))
}
select {
case <-ctx.Done():
s.rmResponseChan(msgID)
return resp, ctx.Err()
case <-s.ctx.Done():
s.rmResponseChan(msgID)
return resp, xerrors.Errorf("vpn protocol closed: %w", s.ctx.Err())
case <-s.recvLoopDone:
logger.Debug(s.ctx, "recvLoopDone while waiting for response")
return resp, io.ErrUnexpectedEOF
case resp = <-respCh:
logger.Debug(s.ctx, "got response", slog.F("resp", resp))
return resp, nil
}
}
func (s *speaker[_, R, _]) newRPC() (uint64, chan R) {
s.mu.Lock()
defer s.mu.Unlock()
msgID := s.nextMsgID
s.nextMsgID++
c := make(chan R, 1)
s.responseChans[msgID] = c
return msgID, c
}
func (s *speaker[_, _, _]) rmResponseChan(msgID uint64) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.responseChans, msgID)
}
func (s *speaker[_, R, _]) tryToDeliverResponse(resp R) {
msgID := resp.GetRpc().GetResponseTo()
s.mu.Lock()
defer s.mu.Unlock()
c, ok := s.responseChans[msgID]
if ok {
c <- resp
// Remove the channel since we delivered a response. This ensures that each response channel
// gets _at most_ one response. Since the channels are buffered with size 1, send will
// never block.
delete(s.responseChans, msgID)
}
}
// handshake performs the initial CoderVPN protocol handshake over the given conn
func handshake(
ctx context.Context, conn io.ReadWriteCloser, logger slog.Logger, me, them SpeakerRole,
) error {
// read and write simultaneously to avoid deadlocking if the conn is not buffered
errCh := make(chan error, 2)
go func() {
ours := headerString(CurrentVersion, me)
_, err := conn.Write([]byte(ours))
logger.Debug(ctx, "wrote out header")
if err != nil {
err = xerrors.Errorf("write header: %w", err)
}
errCh <- err
}()
headerCh := make(chan string, 1)
go func() {
// we can't use bufio.Scanner here because we need to ensure we don't read beyond the
// first newline. So, we'll read one byte at a time. It's inefficient, but the initial
// header is only a few characters, so we'll keep this code simple.
buf := make([]byte, 256)
have := 0
for {
_, err := conn.Read(buf[have : have+1])
if err != nil {
errCh <- xerrors.Errorf("read header: %w", err)
return
}
if buf[have] == '\n' {
logger.Debug(ctx, "got newline header delimiter")
// use have (not have+1) since we don't want the delimiter for verification.
headerCh <- string(buf[:have])
return
}
have++
if have >= len(buf) {
errCh <- xerrors.Errorf("header malformed or too large: %s", string(buf))
return
}
}
}()
writeOK := false
theirHeader := ""
readOK := false
for !(readOK && writeOK) {
select {
case <-ctx.Done():
_ = conn.Close() // ensure our read/write goroutines get a chance to clean up
return ctx.Err()
case err := <-errCh:
if err == nil {
// write goroutine sends nil when completing successfully.
logger.Debug(ctx, "write ok")
writeOK = true
continue
}
_ = conn.Close()
return err
case theirHeader = <-headerCh:
logger.Debug(ctx, "read ok")
readOK = true
}
}
logger.Debug(ctx, "handshake read/write complete", slog.F("their_header", theirHeader))
err := validateHeader(theirHeader, them)
if err != nil {
return xerrors.Errorf("validate header (%s): %w", theirHeader, err)
}
return nil
}
const headerPreamble = "codervpn"
func headerString(version *apiversion.APIVersion, role SpeakerRole) string {
return fmt.Sprintf("%s %s %s\n", headerPreamble, version.String(), role)
}
func validateHeader(header string, expectedRole SpeakerRole) error {
parts := strings.Split(header, " ")
if len(parts) != 3 {
return xerrors.New("wrong number of parts")
}
if parts[0] != headerPreamble {
return xerrors.New("invalid preamble")
}
if err := CurrentVersion.Validate(parts[1]); err != nil {
return xerrors.Errorf("version: %w", err)
}
if parts[2] != string(expectedRole) {
return xerrors.New("unexpected role")
}
return nil
}
type request[S rpcMessage, R rpcMessage] struct {
ctx context.Context
msg R
replyCh chan<- S
}
func (r *request[S, _]) sendReply(reply S) error {
rrpc := reply.EnsureRPC()
mrpc := r.msg.GetRpc()
if mrpc == nil {
return xerrors.Errorf("message didn't want a reply")
}
rrpc.ResponseTo = mrpc.MsgId
select {
case <-r.ctx.Done():
return r.ctx.Err()
case r.replyCh <- reply:
}
return nil
}