chore: implement CoderVPN client & tunnel (#15612)

Addresses #14734.

This PR wires up `tunnel.go` to a `tailnet.Conn` via the new `/tailnet` endpoint, with all the necessary controllers such that a VPN connection can be started, stopped and inspected via the CoderVPN protocol.
This commit is contained in:
Ethan
2024-12-05 13:30:22 +11:00
committed by GitHub
parent b5b0a0e746
commit ba48069325
14 changed files with 1431 additions and 157 deletions
+180
View File
@@ -0,0 +1,180 @@
package vpn
import (
"context"
"net/http"
"net/netip"
"net/url"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"tailscale.com/net/dns"
"tailscale.com/wgengine/router"
"github.com/tailscale/wireguard-go/tun"
"cdr.dev/slog"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/quartz"
)
type Conn interface {
CurrentWorkspaceState() (tailnet.WorkspaceUpdate, error)
Close() error
}
type vpnConn struct {
*tailnet.Conn
cancelFn func()
controller *tailnet.Controller
updatesCtrl *tailnet.TunnelAllWorkspaceUpdatesController
}
func (c *vpnConn) CurrentWorkspaceState() (tailnet.WorkspaceUpdate, error) {
return c.updatesCtrl.CurrentState()
}
func (c *vpnConn) Close() error {
c.cancelFn()
<-c.controller.Closed()
return c.Conn.Close()
}
type client struct{}
type Client interface {
NewConn(ctx context.Context, serverURL *url.URL, token string, options *Options) (Conn, error)
}
func NewClient() Client {
return &client{}
}
type Options struct {
Headers http.Header
Logger slog.Logger
DNSConfigurator dns.OSConfigurator
Router router.Router
TUNFileDescriptor *int
UpdateHandler tailnet.UpdatesHandler
}
func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string, options *Options) (vpnC Conn, err error) {
if options == nil {
options = &Options{}
}
if options.Headers == nil {
options.Headers = http.Header{}
}
var dev tun.Device
if options.TUNFileDescriptor != nil {
// No-op on non-Darwin platforms.
dev, err = makeTUN(*options.TUNFileDescriptor)
if err != nil {
return nil, xerrors.Errorf("make TUN: %w", err)
}
}
headers := options.Headers
sdk := codersdk.New(serverURL)
sdk.SetSessionToken(token)
sdk.HTTPClient.Transport = &codersdk.HeaderTransport{
Transport: http.DefaultTransport,
Header: headers,
}
// New context, separate from initCtx. We don't want to cancel the
// connection if initCtx is canceled.
ctx, cancel := context.WithCancel(context.Background())
defer func() {
if err != nil {
cancel()
}
}()
rpcURL, err := sdk.URL.Parse("/api/v2/tailnet")
if err != nil {
return nil, xerrors.Errorf("parse rpc url: %w", err)
}
me, err := sdk.User(initCtx, codersdk.Me)
if err != nil {
return nil, xerrors.Errorf("get user: %w", err)
}
connInfo, err := workspacesdk.New(sdk).AgentConnectionInfoGeneric(initCtx)
if err != nil {
return nil, xerrors.Errorf("get connection info: %w", err)
}
headers.Set(codersdk.SessionTokenHeader, token)
dialer := workspacesdk.NewWebsocketDialer(options.Logger, rpcURL, &websocket.DialOptions{
HTTPClient: sdk.HTTPClient,
HTTPHeader: headers,
CompressionMode: websocket.CompressionDisabled,
}, workspacesdk.WithWorkspaceUpdates(&proto.WorkspaceUpdatesRequest{
WorkspaceOwnerId: tailnet.UUIDToByteSlice(me.ID),
}))
ip := tailnet.CoderServicePrefix.RandomAddr()
conn, err := tailnet.NewConn(&tailnet.Options{
Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)},
DERPMap: connInfo.DERPMap,
DERPHeader: &headers,
DERPForceWebSockets: connInfo.DERPForceWebSockets,
Logger: options.Logger,
BlockEndpoints: connInfo.DisableDirectConnections,
DNSConfigurator: options.DNSConfigurator,
Router: options.Router,
TUNDev: dev,
})
if err != nil {
return nil, xerrors.Errorf("create tailnet: %w", err)
}
defer func() {
if err != nil {
_ = conn.Close()
}
}()
clk := quartz.NewReal()
controller := tailnet.NewController(options.Logger, dialer)
coordCtrl := tailnet.NewTunnelSrcCoordController(options.Logger, conn)
controller.ResumeTokenCtrl = tailnet.NewBasicResumeTokenController(options.Logger, clk)
controller.CoordCtrl = coordCtrl
controller.DERPCtrl = tailnet.NewBasicDERPController(options.Logger, conn)
updatesCtrl := tailnet.NewTunnelAllWorkspaceUpdatesController(
options.Logger,
coordCtrl,
tailnet.WithDNS(conn, me.Name),
tailnet.WithHandler(options.UpdateHandler),
)
controller.WorkspaceUpdatesCtrl = updatesCtrl
controller.Run(ctx)
options.Logger.Debug(ctx, "running tailnet API v2+ connector")
select {
case <-initCtx.Done():
return nil, xerrors.Errorf("timed out waiting for coordinator and derp map: %w", initCtx.Err())
case err = <-dialer.Connected():
if err != nil {
options.Logger.Error(ctx, "failed to connect to tailnet v2+ API", slog.Error(err))
return nil, xerrors.Errorf("start connector: %w", err)
}
options.Logger.Debug(ctx, "connected to tailnet v2+ API")
}
return &vpnConn{
Conn: conn,
cancelFn: cancel,
controller: controller,
updatesCtrl: updatesCtrl,
}, nil
}
+188
View File
@@ -0,0 +1,188 @@
package vpn_test
import (
"net/http"
"net/http/httptest"
"net/url"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"nhooyr.io/websocket"
"tailscale.com/net/dns"
"tailscale.com/tailcfg"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/coder/v2/tailnet/tailnettest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/vpn"
)
func TestClient_WorkspaceUpdates(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
logger := testutil.Logger(t)
userID := uuid.UUID{1}
wsID := uuid.UUID{2}
peerID := uuid.UUID{3}
fCoord := tailnettest.NewFakeCoordinator()
var coord tailnet.Coordinator = fCoord
coordPtr := atomic.Pointer[tailnet.Coordinator]{}
coordPtr.Store(&coord)
ctrl := gomock.NewController(t)
mProvider := tailnettest.NewMockWorkspaceUpdatesProvider(ctrl)
mSub := tailnettest.NewMockSubscription(ctrl)
outUpdateCh := make(chan *proto.WorkspaceUpdate, 1)
inUpdateCh := make(chan tailnet.WorkspaceUpdate, 1)
mProvider.EXPECT().Subscribe(gomock.Any(), userID).Times(1).Return(mSub, nil)
mSub.EXPECT().Updates().MinTimes(1).Return(outUpdateCh)
mSub.EXPECT().Close().Times(1).Return(nil)
svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{
Logger: logger,
CoordPtr: &coordPtr,
DERPMapUpdateFrequency: time.Hour,
DERPMapFn: func() *tailcfg.DERPMap { return &tailcfg.DERPMap{} },
WorkspaceUpdatesProvider: mProvider,
ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(),
})
require.NoError(t, err)
user := make(chan struct{})
connInfo := make(chan struct{})
serveErrCh := make(chan error)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v2/users/me":
httpapi.Write(ctx, w, http.StatusOK, codersdk.User{
ReducedUser: codersdk.ReducedUser{
MinimalUser: codersdk.MinimalUser{
ID: userID,
},
},
})
user <- struct{}{}
case "/api/v2/workspaceagents/connection":
httpapi.Write(ctx, w, http.StatusOK, workspacesdk.AgentConnectionInfo{
DisableDirectConnections: false,
})
connInfo <- struct{}{}
case "/api/v2/tailnet":
// need 2.3 for WorkspaceUpdates RPC
cVer := r.URL.Query().Get("version")
assert.Equal(t, "2.3", cVer)
sws, err := websocket.Accept(w, r, nil)
if !assert.NoError(t, err) {
return
}
wsCtx, nc := codersdk.WebsocketNetConn(ctx, sws, websocket.MessageBinary)
serveErrCh <- svc.ServeConnV2(wsCtx, nc, tailnet.StreamID{
Name: "client",
ID: peerID,
// Auth can be nil as we use a mock update provider
Auth: tailnet.ClientUserCoordinateeAuth{
Auth: nil,
},
})
default:
http.NotFound(w, r)
}
}))
t.Cleanup(server.Close)
svrURL, err := url.Parse(server.URL)
require.NoError(t, err)
connErrCh := make(chan error)
connCh := make(chan vpn.Conn)
go func() {
conn, err := vpn.NewClient().NewConn(ctx, svrURL, "fakeToken", &vpn.Options{
UpdateHandler: updateHandler(func(wu tailnet.WorkspaceUpdate) error {
inUpdateCh <- wu
return nil
}),
DNSConfigurator: &noopConfigurator{},
})
connErrCh <- err
connCh <- conn
}()
testutil.RequireRecvCtx(ctx, t, user)
testutil.RequireRecvCtx(ctx, t, connInfo)
err = testutil.RequireRecvCtx(ctx, t, connErrCh)
require.NoError(t, err)
conn := testutil.RequireRecvCtx(ctx, t, connCh)
// Send a workspace update
update := &proto.WorkspaceUpdate{
UpsertedWorkspaces: []*proto.Workspace{
{
Id: wsID[:],
},
},
}
testutil.RequireSendCtx(ctx, t, outUpdateCh, update)
// It'll be received by the update handler
recvUpdate := testutil.RequireRecvCtx(ctx, t, inUpdateCh)
require.Len(t, recvUpdate.UpsertedWorkspaces, 1)
require.Equal(t, wsID, recvUpdate.UpsertedWorkspaces[0].ID)
// And be reflected on the Conn's state
state, err := conn.CurrentWorkspaceState()
require.NoError(t, err)
require.Equal(t, tailnet.WorkspaceUpdate{
UpsertedWorkspaces: []*tailnet.Workspace{
{
ID: wsID,
},
},
UpsertedAgents: []*tailnet.Agent{},
DeletedWorkspaces: []*tailnet.Workspace{},
DeletedAgents: []*tailnet.Agent{},
}, state)
// Close the conn
conn.Close()
err = testutil.RequireRecvCtx(ctx, t, serveErrCh)
require.NoError(t, err)
}
type updateHandler func(tailnet.WorkspaceUpdate) error
func (h updateHandler) Update(u tailnet.WorkspaceUpdate) error {
return h(u)
}
type noopConfigurator struct{}
func (*noopConfigurator) Close() error {
return nil
}
func (*noopConfigurator) GetBaseConfig() (dns.OSConfig, error) {
return dns.OSConfig{}, nil
}
func (*noopConfigurator) SetDNS(dns.OSConfig) error {
return nil
}
func (*noopConfigurator) SupportsSplitDNS() bool {
return true
}
var _ dns.OSConfigurator = (*noopConfigurator)(nil)
+6 -3
View File
@@ -22,7 +22,7 @@ const (
)
// OpenTunnel creates a new VPN tunnel by `dup`ing the provided 'PIPE'
// file descriptors for reading, writing, and logging.
// file descriptors for reading and writing.
//
//export OpenTunnel
func OpenTunnel(cReadFD, cWriteFD int32) int32 {
@@ -46,8 +46,11 @@ func OpenTunnel(cReadFD, cWriteFD int32) int32 {
return ErrOpenPipe
}
// Logs will be sent over the protocol
_, err = vpn.NewTunnel(ctx, slog.Make(), conn)
_, err = vpn.NewTunnel(ctx, slog.Make(), conn, vpn.NewClient(),
vpn.UseAsDNSConfig(),
vpn.UseAsRouter(),
vpn.UseAsLogger(),
)
if err != nil {
unix.Close(readFD)
unix.Close(writeFD)
+10
View File
@@ -0,0 +1,10 @@
//go:build !darwin
package vpn
import "github.com/tailscale/wireguard-go/tun"
// This is a no-op on non-Darwin platforms.
func makeTUN(int) (tun.Device, error) {
return nil, nil
}
+30
View File
@@ -0,0 +1,30 @@
//go:build darwin
package vpn
import (
"os"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/unix"
"golang.org/x/xerrors"
)
func makeTUN(tunFD int) (tun.Device, error) {
dupTunFd, err := unix.Dup(tunFD)
if err != nil {
return nil, xerrors.Errorf("dup tun fd: %w", err)
}
err = unix.SetNonblock(dupTunFd, true)
if err != nil {
unix.Close(dupTunFd)
return nil, xerrors.Errorf("set nonblock: %w", err)
}
fileTun, err := tun.CreateTUNFromFile(os.NewFile(uintptr(dupTunFd), "/dev/tun"), 0)
if err != nil {
unix.Close(dupTunFd)
return nil, xerrors.Errorf("create TUN from File: %w", err)
}
return fileTun, nil
}
+195 -18
View File
@@ -6,32 +6,56 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"reflect"
"strconv"
"sync"
"unicode"
"golang.org/x/xerrors"
"tailscale.com/net/dns"
"tailscale.com/wgengine/router"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/tailnet"
)
type Tunnel struct {
speaker[*TunnelMessage, *ManagerMessage, ManagerMessage]
ctx context.Context
logger slog.Logger
requestLoopDone chan struct{}
logger slog.Logger
logMu sync.Mutex
logs []*TunnelMessage
client Client
conn Conn
// clientLogger is a separate logger than `logger` when the `UseAsLogger`
// option is used, to avoid the tunnel using itself as a sink for it's own
// logs, which could lead to deadlocks.
clientLogger slog.Logger
// router and dnsConfigurator may be nil
router router.Router
dnsConfigurator dns.OSConfigurator
}
type TunnelOption func(t *Tunnel)
func NewTunnel(
ctx context.Context, logger slog.Logger, conn io.ReadWriteCloser,
ctx context.Context,
logger slog.Logger,
mgrConn io.ReadWriteCloser,
client Client,
opts ...TunnelOption,
) (*Tunnel, error) {
logger = logger.Named("vpn")
s, err := newSpeaker[*TunnelMessage, *ManagerMessage](
ctx, logger, conn, SpeakerRoleTunnel, SpeakerRoleManager)
ctx, logger, mgrConn, SpeakerRoleTunnel, SpeakerRoleManager)
if err != nil {
return nil, err
}
@@ -40,7 +64,13 @@ func NewTunnel(
speaker: *(s),
ctx: ctx,
logger: logger,
clientLogger: logger,
requestLoopDone: make(chan struct{}),
client: client,
}
for _, opt := range opts {
opt(t)
}
t.speaker.start()
go t.requestLoop()
@@ -55,6 +85,14 @@ func (t *Tunnel) requestLoop() {
if err := req.sendReply(resp); err != nil {
t.logger.Debug(t.ctx, "failed to send RPC reply", slog.Error(err))
}
if _, ok := resp.GetMsg().(*TunnelMessage_Stop); ok {
// TODO: Wait for the reply to be sent before closing the speaker.
// err := t.speaker.Close()
// if err != nil {
// t.logger.Error(t.ctx, "failed to close speaker", slog.Error(err))
// }
return
}
continue
}
// Not a unary RPC. We don't know of any message types that are neither a response nor a
@@ -70,12 +108,12 @@ func (t *Tunnel) handleRPC(req *ManagerMessage, msgID uint64) *TunnelMessage {
resp.Rpc = &RPC{ResponseTo: msgID}
switch msg := req.GetMsg().(type) {
case *ManagerMessage_GetPeerUpdate:
// TODO: actually get the peer updates
state, err := t.conn.CurrentWorkspaceState()
if err != nil {
t.logger.Critical(t.ctx, "failed to get current workspace state", slog.Error(err))
}
resp.Msg = &TunnelMessage_PeerUpdate{
PeerUpdate: &PeerUpdate{
UpsertedWorkspaces: nil,
UpsertedAgents: nil,
},
PeerUpdate: convertWorkspaceUpdate(state),
}
return resp
case *ManagerMessage_Start:
@@ -84,27 +122,35 @@ func (t *Tunnel) handleRPC(req *ManagerMessage, msgID uint64) *TunnelMessage {
slog.F("url", startReq.CoderUrl),
slog.F("tunnel_fd", startReq.TunnelFileDescriptor),
)
// TODO: actually start the tunnel
err := t.start(startReq)
var errStr string
if err != nil {
t.logger.Error(t.ctx, "failed to start tunnel", slog.Error(err))
errStr = err.Error()
}
resp.Msg = &TunnelMessage_Start{
Start: &StartResponse{
Success: true,
Success: err == nil,
ErrorMessage: errStr,
},
}
return resp
case *ManagerMessage_Stop:
t.logger.Info(t.ctx, "stopping CoderVPN tunnel")
// TODO: actually stop the tunnel
resp.Msg = &TunnelMessage_Stop{
Stop: &StopResponse{
Success: true,
},
}
err := t.speaker.Close()
err := t.stop(msg.Stop)
var errStr string
if err != nil {
t.logger.Error(t.ctx, "failed to close speaker", slog.Error(err))
t.logger.Error(t.ctx, "failed to stop tunnel", slog.Error(err))
errStr = err.Error()
} else {
t.logger.Info(t.ctx, "coderVPN tunnel stopped")
}
resp.Msg = &TunnelMessage_Stop{
Stop: &StopResponse{
Success: err == nil,
ErrorMessage: errStr,
},
}
return resp
default:
t.logger.Warn(t.ctx, "unhandled manager request", slog.F("request", msg))
@@ -112,6 +158,24 @@ func (t *Tunnel) handleRPC(req *ManagerMessage, msgID uint64) *TunnelMessage {
}
}
func UseAsRouter() TunnelOption {
return func(t *Tunnel) {
t.router = NewRouter(t)
}
}
func UseAsLogger() TunnelOption {
return func(t *Tunnel) {
t.clientLogger = slog.Make(t)
}
}
func UseAsDNSConfig() TunnelOption {
return func(t *Tunnel) {
t.dnsConfigurator = NewDNSConfigurator(t)
}
}
// ApplyNetworkSettings sends a request to the manager to apply the given network settings
func (t *Tunnel) ApplyNetworkSettings(ctx context.Context, ns *NetworkSettingsRequest) error {
msg, err := t.speaker.unaryRPC(ctx, &TunnelMessage{
@@ -129,6 +193,65 @@ func (t *Tunnel) ApplyNetworkSettings(ctx context.Context, ns *NetworkSettingsRe
return nil
}
func (t *Tunnel) Update(update tailnet.WorkspaceUpdate) error {
msg := &TunnelMessage{
Msg: &TunnelMessage_PeerUpdate{
PeerUpdate: convertWorkspaceUpdate(update),
},
}
select {
case <-t.ctx.Done():
return t.ctx.Err()
case t.sendCh <- msg:
}
return nil
}
func (t *Tunnel) start(req *StartRequest) error {
rawURL := req.GetCoderUrl()
if rawURL == "" {
return xerrors.New("missing coder url")
}
svrURL, err := url.Parse(rawURL)
if err != nil {
return xerrors.Errorf("parse url %q: %w", rawURL, err)
}
apiToken := req.GetApiToken()
if apiToken == "" {
return xerrors.New("missing api token")
}
var header http.Header
for _, h := range req.GetHeaders() {
header.Add(h.GetName(), h.GetValue())
}
if t.conn == nil {
t.conn, err = t.client.NewConn(
t.ctx,
svrURL,
apiToken,
&Options{
Headers: header,
Logger: t.clientLogger,
DNSConfigurator: t.dnsConfigurator,
Router: t.router,
TUNFileDescriptor: ptr.Ref(int(req.GetTunnelFileDescriptor())),
UpdateHandler: t,
},
)
} else {
t.logger.Warn(t.ctx, "asked to start tunnel, but tunnel is already running")
}
return err
}
func (t *Tunnel) stop(*StopRequest) error {
if t.conn == nil {
return nil
}
return t.conn.Close()
}
var _ slog.Sink = &Tunnel{}
func (t *Tunnel) LogEntry(_ context.Context, e slog.SinkEntry) {
@@ -170,6 +293,60 @@ func sinkEntryToPb(e slog.SinkEntry) *Log {
return l
}
func convertWorkspaceUpdate(update tailnet.WorkspaceUpdate) *PeerUpdate {
out := &PeerUpdate{
UpsertedWorkspaces: make([]*Workspace, len(update.UpsertedWorkspaces)),
UpsertedAgents: make([]*Agent, len(update.UpsertedAgents)),
DeletedWorkspaces: make([]*Workspace, len(update.DeletedWorkspaces)),
DeletedAgents: make([]*Agent, len(update.DeletedAgents)),
}
for i, ws := range update.UpsertedWorkspaces {
out.UpsertedWorkspaces[i] = &Workspace{
Id: tailnet.UUIDToByteSlice(ws.ID),
Name: ws.Name,
Status: Workspace_Status(ws.Status),
}
}
for i, agent := range update.UpsertedAgents {
fqdn := make([]string, 0, len(agent.Hosts))
for name := range agent.Hosts {
fqdn = append(fqdn, name.WithTrailingDot())
}
out.UpsertedAgents[i] = &Agent{
Id: tailnet.UUIDToByteSlice(agent.ID),
Name: agent.Name,
WorkspaceId: tailnet.UUIDToByteSlice(agent.WorkspaceID),
Fqdn: fqdn,
IpAddrs: []string{tailnet.CoderServicePrefix.AddrFromUUID(agent.ID).String()},
// TODO: Populate
LastHandshake: nil,
}
}
for i, ws := range update.DeletedWorkspaces {
out.DeletedWorkspaces[i] = &Workspace{
Id: tailnet.UUIDToByteSlice(ws.ID),
Name: ws.Name,
Status: Workspace_Status(ws.Status),
}
}
for i, agent := range update.DeletedAgents {
fqdn := make([]string, 0, len(agent.Hosts))
for name := range agent.Hosts {
fqdn = append(fqdn, name.WithTrailingDot())
}
out.DeletedAgents[i] = &Agent{
Id: tailnet.UUIDToByteSlice(agent.ID),
Name: agent.Name,
WorkspaceId: tailnet.UUIDToByteSlice(agent.WorkspaceID),
Fqdn: fqdn,
IpAddrs: []string{tailnet.CoderServicePrefix.AddrFromUUID(agent.ID).String()},
// TODO: Populate
LastHandshake: nil,
}
}
return out
}
// the following are taken from sloghuman:
func formatValue(v interface{}) string {
+280
View File
@@ -0,0 +1,280 @@
package vpn
import (
"context"
"net"
"net/url"
"sync"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/testutil"
)
func newFakeClient(ctx context.Context, t *testing.T) *fakeClient {
return &fakeClient{
t: t,
ctx: ctx,
ch: make(chan *fakeConn, 1),
}
}
type fakeClient struct {
t *testing.T
ctx context.Context
ch chan *fakeConn
}
var _ Client = (*fakeClient)(nil)
func (f *fakeClient) NewConn(context.Context, *url.URL, string, *Options) (Conn, error) {
select {
case <-f.ctx.Done():
return nil, f.ctx.Err()
case conn := <-f.ch:
return conn, nil
}
}
func newFakeConn(state tailnet.WorkspaceUpdate) *fakeConn {
return &fakeConn{
closed: make(chan struct{}),
state: state,
}
}
type fakeConn struct {
state tailnet.WorkspaceUpdate
closed chan struct{}
doClose sync.Once
}
var _ Conn = (*fakeConn)(nil)
func (f *fakeConn) CurrentWorkspaceState() (tailnet.WorkspaceUpdate, error) {
return f.state, nil
}
func (f *fakeConn) Close() error {
f.doClose.Do(func() {
close(f.closed)
})
return nil
}
func TestTunnel_StartStop(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
client := newFakeClient(ctx, t)
conn := newFakeConn(tailnet.WorkspaceUpdate{})
_, mgr := setupTunnel(t, ctx, client)
errCh := make(chan error, 1)
var resp *TunnelMessage
// When: we start the tunnel
go func() {
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
Msg: &ManagerMessage_Start{
Start: &StartRequest{
TunnelFileDescriptor: 2,
CoderUrl: "https://coder.example.com",
ApiToken: "fakeToken",
},
},
})
resp = r
errCh <- err
}()
// Then: `NewConn` is called,
testutil.RequireSendCtx(ctx, t, client.ch, conn)
// And: a response is received
err := testutil.RequireRecvCtx(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
require.True(t, ok)
// When: we stop the tunnel
go func() {
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
Msg: &ManagerMessage_Stop{},
})
resp = r
errCh <- err
}()
// Then: `Close` is called on the connection
testutil.RequireRecvCtx(ctx, t, conn.closed)
// And: a Stop response is received
err = testutil.RequireRecvCtx(ctx, t, errCh)
require.NoError(t, err)
_, ok = resp.Msg.(*TunnelMessage_Stop)
require.True(t, ok)
err = mgr.Close()
require.NoError(t, err)
}
func TestTunnel_PeerUpdate(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
wsID1 := uuid.UUID{1}
wsID2 := uuid.UUID{2}
client := newFakeClient(ctx, t)
conn := newFakeConn(tailnet.WorkspaceUpdate{
UpsertedWorkspaces: []*tailnet.Workspace{
{
ID: wsID1,
},
{
ID: wsID2,
},
},
})
tun, mgr := setupTunnel(t, ctx, client)
errCh := make(chan error, 1)
var resp *TunnelMessage
go func() {
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
Msg: &ManagerMessage_Start{
Start: &StartRequest{
TunnelFileDescriptor: 2,
CoderUrl: "https://coder.example.com",
ApiToken: "fakeToken",
},
},
})
resp = r
errCh <- err
}()
testutil.RequireSendCtx(ctx, t, client.ch, conn)
err := testutil.RequireRecvCtx(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
require.True(t, ok)
err = tun.Update(tailnet.WorkspaceUpdate{
UpsertedWorkspaces: []*tailnet.Workspace{
{
ID: wsID2,
},
},
})
require.NoError(t, err)
// Then: the tunnel sends a PeerUpdate message
req := testutil.RequireRecvCtx(ctx, t, mgr.requests)
require.Nil(t, req.msg.Rpc)
require.NotNil(t, req.msg.GetPeerUpdate())
require.Len(t, req.msg.GetPeerUpdate().UpsertedWorkspaces, 1)
require.Equal(t, wsID2[:], req.msg.GetPeerUpdate().UpsertedWorkspaces[0].Id)
// When: the manager requests a PeerUpdate
go func() {
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
Msg: &ManagerMessage_GetPeerUpdate{},
})
resp = r
errCh <- err
}()
// Then: a PeerUpdate message is sent using the Conn's state
err = testutil.RequireRecvCtx(ctx, t, errCh)
require.NoError(t, err)
_, ok = resp.Msg.(*TunnelMessage_PeerUpdate)
require.True(t, ok)
require.Len(t, resp.GetPeerUpdate().UpsertedWorkspaces, 2)
require.Equal(t, wsID1[:], resp.GetPeerUpdate().UpsertedWorkspaces[0].Id)
require.Equal(t, wsID2[:], resp.GetPeerUpdate().UpsertedWorkspaces[1].Id)
}
func TestTunnel_NetworkSettings(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
client := newFakeClient(ctx, t)
conn := newFakeConn(tailnet.WorkspaceUpdate{})
tun, mgr := setupTunnel(t, ctx, client)
errCh := make(chan error, 1)
var resp *TunnelMessage
go func() {
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
Msg: &ManagerMessage_Start{
Start: &StartRequest{
TunnelFileDescriptor: 2,
CoderUrl: "https://coder.example.com",
ApiToken: "fakeToken",
},
},
})
resp = r
errCh <- err
}()
testutil.RequireSendCtx(ctx, t, client.ch, conn)
err := testutil.RequireRecvCtx(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
require.True(t, ok)
// When: we inform the tunnel of network settings
go func() {
err := tun.ApplyNetworkSettings(ctx, &NetworkSettingsRequest{
Mtu: 1200,
})
errCh <- err
}()
// Then: the tunnel sends a NetworkSettings message
req := testutil.RequireRecvCtx(ctx, t, mgr.requests)
require.NotNil(t, req.msg.Rpc)
require.Equal(t, uint32(1200), req.msg.GetNetworkSettings().Mtu)
go func() {
testutil.RequireSendCtx(ctx, t, mgr.sendCh, &ManagerMessage{
Rpc: &RPC{ResponseTo: req.msg.Rpc.MsgId},
Msg: &ManagerMessage_NetworkSettings{
NetworkSettings: &NetworkSettingsResponse{
Success: true,
},
},
})
}()
// And: `ApplyNetworkSettings` returns without error once the manager responds
err = testutil.RequireRecvCtx(ctx, t, errCh)
require.NoError(t, err)
}
//nolint:revive // t takes precedence
func setupTunnel(t *testing.T, ctx context.Context, client *fakeClient) (*Tunnel, *speaker[*ManagerMessage, *TunnelMessage, TunnelMessage]) {
mp, tp := net.Pipe()
t.Cleanup(func() { _ = mp.Close() })
t.Cleanup(func() { _ = tp.Close() })
logger := testutil.Logger(t)
var tun *Tunnel
var mgr *speaker[*ManagerMessage, *TunnelMessage, TunnelMessage]
errCh := make(chan error, 2)
go func() {
tunnel, err := NewTunnel(ctx, logger.Named("tunnel"), tp, client)
tun = tunnel
errCh <- err
}()
go func() {
manager, err := newSpeaker[*ManagerMessage, *TunnelMessage](ctx, logger.Named("manager"), mp, SpeakerRoleManager, SpeakerRoleTunnel)
mgr = manager
errCh <- err
}()
err := testutil.RequireRecvCtx(ctx, t, errCh)
require.NoError(t, err)
err = testutil.RequireRecvCtx(ctx, t, errCh)
require.NoError(t, err)
mgr.start()
return tun, mgr
}
+129 -44
View File
@@ -718,7 +718,7 @@ type Agent struct {
Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
WorkspaceId []byte `protobuf:"bytes,3,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` // UUID
Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"`
Fqdn []string `protobuf:"bytes,4,rep,name=fqdn,proto3" json:"fqdn,omitempty"`
IpAddrs []string `protobuf:"bytes,5,rep,name=ip_addrs,json=ipAddrs,proto3" json:"ip_addrs,omitempty"`
// last_handshake is the primary indicator of whether we are connected to a peer. Zero value or
// anything longer than 5 minutes ago means there is a problem.
@@ -778,11 +778,11 @@ func (x *Agent) GetWorkspaceId() []byte {
return nil
}
func (x *Agent) GetFqdn() string {
func (x *Agent) GetFqdn() []string {
if x != nil {
return x.Fqdn
}
return ""
return nil
}
func (x *Agent) GetIpAddrs() []string {
@@ -953,9 +953,10 @@ type StartRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
TunnelFileDescriptor int32 `protobuf:"varint,1,opt,name=tunnel_file_descriptor,json=tunnelFileDescriptor,proto3" json:"tunnel_file_descriptor,omitempty"`
CoderUrl string `protobuf:"bytes,2,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"`
ApiToken string `protobuf:"bytes,3,opt,name=api_token,json=apiToken,proto3" json:"api_token,omitempty"`
TunnelFileDescriptor int32 `protobuf:"varint,1,opt,name=tunnel_file_descriptor,json=tunnelFileDescriptor,proto3" json:"tunnel_file_descriptor,omitempty"`
CoderUrl string `protobuf:"bytes,2,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"`
ApiToken string `protobuf:"bytes,3,opt,name=api_token,json=apiToken,proto3" json:"api_token,omitempty"`
Headers []*StartRequest_Header `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty"`
}
func (x *StartRequest) Reset() {
@@ -1011,6 +1012,13 @@ func (x *StartRequest) GetApiToken() string {
return ""
}
func (x *StartRequest) GetHeaders() []*StartRequest_Header {
if x != nil {
return x.Headers
}
return nil
}
type StartResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1579,6 +1587,62 @@ func (x *NetworkSettingsRequest_IPv6Settings_IPv6Route) GetRouter() string {
return ""
}
// Additional HTTP headers added to all requests
type StartRequest_Header struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *StartRequest_Header) Reset() {
*x = StartRequest_Header{}
if protoimpl.UnsafeEnabled {
mi := &file_vpn_vpn_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *StartRequest_Header) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StartRequest_Header) ProtoMessage() {}
func (x *StartRequest_Header) ProtoReflect() protoreflect.Message {
mi := &file_vpn_vpn_proto_msgTypes[20]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StartRequest_Header.ProtoReflect.Descriptor instead.
func (*StartRequest_Header) Descriptor() ([]byte, []int) {
return file_vpn_vpn_proto_rawDescGZIP(), []int{10, 0}
}
func (x *StartRequest_Header) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *StartRequest_Header) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
var File_vpn_vpn_proto protoreflect.FileDescriptor
var file_vpn_vpn_proto_rawDesc = []byte{
@@ -1680,7 +1744,7 @@ var file_vpn_vpn_proto_rawDesc = []byte{
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63,
0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b,
0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x69,
0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x69,
0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x69,
0x70, 0x41, 0x64, 0x64, 0x72, 0x73, 0x12, 0x41, 0x0a, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68,
0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
@@ -1775,30 +1839,37 @@ var file_vpn_vpn_proto_rawDesc = []byte{
0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73,
0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65,
0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x7e, 0x0a, 0x0c, 0x53,
0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x16, 0x74,
0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72,
0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x74, 0x75, 0x6e,
0x6e, 0x65, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f,
0x72, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1b,
0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x61, 0x70, 0x69, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x4e, 0x0a, 0x0d, 0x53,
0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07,
0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73,
0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65,
0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x53,
0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x0c, 0x53, 0x74,
0x6f, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75,
0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63,
0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72,
0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x39, 0x5a, 0x1d, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f,
0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x76, 0x70, 0x6e, 0xaa, 0x02, 0x17, 0x43, 0x6f, 0x64,
0x65, 0x72, 0x2e, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x2e, 0x56, 0x70, 0x6e, 0x2e, 0x50,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xe6, 0x01, 0x0a, 0x0c,
0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x16,
0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x64, 0x65, 0x73, 0x63,
0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x74, 0x75,
0x6e, 0x6e, 0x65, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12,
0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x69, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x32, 0x0a, 0x07,
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e,
0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73,
0x1a, 0x32, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14,
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x22, 0x4e, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73,
0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12,
0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01,
0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a,
0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x42, 0x39, 0x5a, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f,
0x76, 0x70, 0x6e, 0xaa, 0x02, 0x17, 0x43, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x73, 0x6b,
0x74, 0x6f, 0x70, 0x2e, 0x56, 0x70, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -1814,7 +1885,7 @@ func file_vpn_vpn_proto_rawDescGZIP() []byte {
}
var file_vpn_vpn_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_vpn_vpn_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
var file_vpn_vpn_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
var file_vpn_vpn_proto_goTypes = []interface{}{
(Log_Level)(0), // 0: vpn.Log.Level
(Workspace_Status)(0), // 1: vpn.Workspace.Status
@@ -1838,7 +1909,8 @@ var file_vpn_vpn_proto_goTypes = []interface{}{
(*NetworkSettingsRequest_IPv6Settings)(nil), // 19: vpn.NetworkSettingsRequest.IPv6Settings
(*NetworkSettingsRequest_IPv4Settings_IPv4Route)(nil), // 20: vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route
(*NetworkSettingsRequest_IPv6Settings_IPv6Route)(nil), // 21: vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route
(*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp
(*StartRequest_Header)(nil), // 22: vpn.StartRequest.Header
(*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp
}
var file_vpn_vpn_proto_depIdxs = []int32{
2, // 0: vpn.ManagerMessage.rpc:type_name -> vpn.RPC
@@ -1859,19 +1931,20 @@ var file_vpn_vpn_proto_depIdxs = []int32{
8, // 15: vpn.PeerUpdate.deleted_workspaces:type_name -> vpn.Workspace
9, // 16: vpn.PeerUpdate.deleted_agents:type_name -> vpn.Agent
1, // 17: vpn.Workspace.status:type_name -> vpn.Workspace.Status
22, // 18: vpn.Agent.last_handshake:type_name -> google.protobuf.Timestamp
23, // 18: vpn.Agent.last_handshake:type_name -> google.protobuf.Timestamp
17, // 19: vpn.NetworkSettingsRequest.dns_settings:type_name -> vpn.NetworkSettingsRequest.DNSSettings
18, // 20: vpn.NetworkSettingsRequest.ipv4_settings:type_name -> vpn.NetworkSettingsRequest.IPv4Settings
19, // 21: vpn.NetworkSettingsRequest.ipv6_settings:type_name -> vpn.NetworkSettingsRequest.IPv6Settings
20, // 22: vpn.NetworkSettingsRequest.IPv4Settings.included_routes:type_name -> vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route
20, // 23: vpn.NetworkSettingsRequest.IPv4Settings.excluded_routes:type_name -> vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route
21, // 24: vpn.NetworkSettingsRequest.IPv6Settings.included_routes:type_name -> vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route
21, // 25: vpn.NetworkSettingsRequest.IPv6Settings.excluded_routes:type_name -> vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route
26, // [26:26] is the sub-list for method output_type
26, // [26:26] is the sub-list for method input_type
26, // [26:26] is the sub-list for extension type_name
26, // [26:26] is the sub-list for extension extendee
0, // [0:26] is the sub-list for field type_name
22, // 22: vpn.StartRequest.headers:type_name -> vpn.StartRequest.Header
20, // 23: vpn.NetworkSettingsRequest.IPv4Settings.included_routes:type_name -> vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route
20, // 24: vpn.NetworkSettingsRequest.IPv4Settings.excluded_routes:type_name -> vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route
21, // 25: vpn.NetworkSettingsRequest.IPv6Settings.included_routes:type_name -> vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route
21, // 26: vpn.NetworkSettingsRequest.IPv6Settings.excluded_routes:type_name -> vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route
27, // [27:27] is the sub-list for method output_type
27, // [27:27] is the sub-list for method input_type
27, // [27:27] is the sub-list for extension type_name
27, // [27:27] is the sub-list for extension extendee
0, // [0:27] is the sub-list for field type_name
}
func init() { file_vpn_vpn_proto_init() }
@@ -2120,6 +2193,18 @@ func file_vpn_vpn_proto_init() {
return nil
}
}
file_vpn_vpn_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StartRequest_Header); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_vpn_vpn_proto_msgTypes[1].OneofWrappers = []interface{}{
(*ManagerMessage_GetPeerUpdate)(nil),
@@ -2140,7 +2225,7 @@ func file_vpn_vpn_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_vpn_vpn_proto_rawDesc,
NumEnums: 2,
NumMessages: 20,
NumMessages: 21,
NumExtensions: 0,
NumServices: 0,
},
+7 -1
View File
@@ -105,7 +105,7 @@ message Agent {
bytes id = 1; // UUID
string name = 2;
bytes workspace_id = 3; // UUID
string fqdn = 4;
repeated string fqdn = 4;
repeated string ip_addrs = 5;
// last_handshake is the primary indicator of whether we are connected to a peer. Zero value or
// anything longer than 5 minutes ago means there is a problem.
@@ -179,6 +179,12 @@ message StartRequest {
int32 tunnel_file_descriptor = 1;
string coder_url = 2;
string api_token = 3;
// Additional HTTP headers added to all requests
message Header {
string name = 1;
string value = 2;
}
repeated Header headers = 4;
}
message StartResponse {