Files
coder/tailnet/service_test.go
T
Spike Curtis 25f2abf9ab chore: remove tailnet from agent API and rename client API to tailnet (#11303)
Refactors our DRPC service definitions slightly.

In the previous version, I inserted the RPCs from the tailnet proto directly into the Agent service.  This makes things hard to deal with because DRPC then generates a new set of methods with new interfaces with the `DRPCAgent_` prefixed.  Since you can't have a single method that takes different argument types, we couldn't reuse the implementation of those RFCs without a lot of extra classes and pass-thru methods.

Instead, the "right" way to do it is to integrate at the DRPC layer.  So, we have two DRPC services available over the Agent websocket, and register them both on the DRPC `mux`.

Since the tailnet proto RPC service is now for both clients and agents, I renamed some things to clarify and shorten.

This PR also removes the `TailnetAPI` implementation from the `agentapi` package, and the next PR in the stack replaces it with the implementation from the `tailnet` package.
2024-01-02 10:02:45 +04:00

285 lines
6.8 KiB
Go

package tailnet_test
import (
"context"
"fmt"
"io"
"net"
"net/http"
"sync/atomic"
"testing"
"time"
"golang.org/x/xerrors"
"tailscale.com/tailcfg"
"github.com/google/uuid"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/coder/v2/testutil"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/tailnet"
)
func TestValidateVersion(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
version string
supported bool
}{
{
name: "Current",
version: fmt.Sprintf("%d.%d", tailnet.CurrentMajor, tailnet.CurrentMinor),
supported: true,
},
{
name: "TooNewMinor",
version: fmt.Sprintf("%d.%d", tailnet.CurrentMajor, tailnet.CurrentMinor+1),
},
{
name: "TooNewMajor",
version: fmt.Sprintf("%d.%d", tailnet.CurrentMajor+1, tailnet.CurrentMinor),
},
{
name: "1.0",
version: "1.0",
supported: true,
},
{
name: "2.0",
version: "2.0",
supported: true,
},
{
name: "Malformed0",
version: "cats",
},
{
name: "Malformed1",
version: "cats.dogs",
},
{
name: "Malformed2",
version: "1.0.1",
},
{
name: "Malformed3",
version: "11",
},
{
name: "TooOld",
version: "0.8",
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := tailnet.ValidateVersion(tc.version)
if tc.supported {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}
func TestClientService_ServeClient_V2(t *testing.T) {
t.Parallel()
fCoord := newFakeCoordinator()
var coord tailnet.Coordinator = fCoord
coordPtr := atomic.Pointer[tailnet.Coordinator]{}
coordPtr.Store(&coord)
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
derpMap := &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{999: {RegionCode: "test"}}}
uut, err := tailnet.NewClientService(
logger, &coordPtr,
time.Millisecond, func() *tailcfg.DERPMap { return derpMap },
)
require.NoError(t, err)
ctx := testutil.Context(t, testutil.WaitShort)
c, s := net.Pipe()
defer c.Close()
defer s.Close()
clientID := uuid.MustParse("10000001-0000-0000-0000-000000000000")
agentID := uuid.MustParse("20000001-0000-0000-0000-000000000000")
errCh := make(chan error, 1)
go func() {
err := uut.ServeClient(ctx, "2.0", s, clientID, agentID)
t.Logf("ServeClient returned; err=%v", err)
errCh <- err
}()
client, err := tailnet.NewDRPCClient(c)
require.NoError(t, err)
// Coordinate
stream, err := client.Coordinate(ctx)
require.NoError(t, err)
defer stream.Close()
err = stream.Send(&proto.CoordinateRequest{
UpdateSelf: &proto.CoordinateRequest_UpdateSelf{Node: &proto.Node{PreferredDerp: 11}},
})
require.NoError(t, err)
call := testutil.RequireRecvCtx(ctx, t, fCoord.coordinateCalls)
require.NotNil(t, call)
require.Equal(t, call.id, clientID)
require.Equal(t, call.name, "client")
require.True(t, call.auth.Authorize(agentID))
req := testutil.RequireRecvCtx(ctx, t, call.reqs)
require.Equal(t, int32(11), req.GetUpdateSelf().GetNode().GetPreferredDerp())
call.resps <- &proto.CoordinateResponse{PeerUpdates: []*proto.CoordinateResponse_PeerUpdate{
{
Kind: proto.CoordinateResponse_PeerUpdate_NODE,
Node: &proto.Node{PreferredDerp: 22},
Id: agentID[:],
},
}}
resp, err := stream.Recv()
require.NoError(t, err)
u := resp.GetPeerUpdates()
require.Len(t, u, 1)
require.Equal(t, int32(22), u[0].GetNode().GetPreferredDerp())
err = stream.Close()
require.NoError(t, err)
// DERP Map
dms, err := client.StreamDERPMaps(ctx, &proto.StreamDERPMapsRequest{})
require.NoError(t, err)
gotDermMap, err := dms.Recv()
require.NoError(t, err)
require.Equal(t, "test", gotDermMap.GetRegions()[999].GetRegionCode())
err = dms.Close()
require.NoError(t, err)
// RPCs closed; we need to close the Conn to end the session.
err = c.Close()
require.NoError(t, err)
err = testutil.RequireRecvCtx(ctx, t, errCh)
require.ErrorIs(t, err, io.EOF)
}
func TestClientService_ServeClient_V1(t *testing.T) {
t.Parallel()
fCoord := newFakeCoordinator()
var coord tailnet.Coordinator = fCoord
coordPtr := atomic.Pointer[tailnet.Coordinator]{}
coordPtr.Store(&coord)
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
uut, err := tailnet.NewClientService(logger, &coordPtr, 0, nil)
require.NoError(t, err)
ctx := testutil.Context(t, testutil.WaitShort)
c, s := net.Pipe()
defer c.Close()
defer s.Close()
clientID := uuid.MustParse("10000001-0000-0000-0000-000000000000")
agentID := uuid.MustParse("20000001-0000-0000-0000-000000000000")
errCh := make(chan error, 1)
go func() {
err := uut.ServeClient(ctx, "1.0", s, clientID, agentID)
t.Logf("ServeClient returned; err=%v", err)
errCh <- err
}()
call := testutil.RequireRecvCtx(ctx, t, fCoord.serveClientCalls)
require.NotNil(t, call)
require.Equal(t, call.id, clientID)
require.Equal(t, call.agent, agentID)
require.Equal(t, s, call.conn)
expectedError := xerrors.New("test error")
select {
case call.errCh <- expectedError:
// ok!
case <-ctx.Done():
t.Fatalf("timeout sending error")
}
err = testutil.RequireRecvCtx(ctx, t, errCh)
require.ErrorIs(t, err, expectedError)
}
type fakeCoordinator struct {
coordinateCalls chan *fakeCoordinate
serveClientCalls chan *fakeServeClient
}
func (*fakeCoordinator) ServeHTTPDebug(http.ResponseWriter, *http.Request) {
panic("unimplemented")
}
func (*fakeCoordinator) Node(uuid.UUID) *tailnet.Node {
panic("unimplemented")
}
func (f *fakeCoordinator) ServeClient(conn net.Conn, id uuid.UUID, agent uuid.UUID) error {
errCh := make(chan error)
f.serveClientCalls <- &fakeServeClient{
conn: conn,
id: id,
agent: agent,
errCh: errCh,
}
return <-errCh
}
func (*fakeCoordinator) ServeAgent(net.Conn, uuid.UUID, string) error {
panic("unimplemented")
}
func (*fakeCoordinator) Close() error {
panic("unimplemented")
}
func (*fakeCoordinator) ServeMultiAgent(uuid.UUID) tailnet.MultiAgentConn {
panic("unimplemented")
}
func (f *fakeCoordinator) Coordinate(ctx context.Context, id uuid.UUID, name string, a tailnet.TunnelAuth) (chan<- *proto.CoordinateRequest, <-chan *proto.CoordinateResponse) {
reqs := make(chan *proto.CoordinateRequest, 100)
resps := make(chan *proto.CoordinateResponse, 100)
f.coordinateCalls <- &fakeCoordinate{
ctx: ctx,
id: id,
name: name,
auth: a,
reqs: reqs,
resps: resps,
}
return reqs, resps
}
func newFakeCoordinator() *fakeCoordinator {
return &fakeCoordinator{
coordinateCalls: make(chan *fakeCoordinate, 100),
serveClientCalls: make(chan *fakeServeClient, 100),
}
}
type fakeCoordinate struct {
ctx context.Context
id uuid.UUID
name string
auth tailnet.TunnelAuth
reqs chan *proto.CoordinateRequest
resps chan *proto.CoordinateResponse
}
type fakeServeClient struct {
conn net.Conn
id uuid.UUID
agent uuid.UUID
errCh chan error
}