From 7a7a8657dd7a35632edbc6ec4aefd2b9847ebd1c Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 12 Aug 2024 06:56:30 +0000 Subject: [PATCH 1/6] feat: add resume support to coordinator connections Inspired by other real time apps, the coordinator RPC API now has a "RefreshResumeToken" RPC that issues a JWT that can be used on reconnect to persist the same client peer ID. --- cli/server.go | 42 +- coderd/coderd.go | 10 + coderd/coderdtest/coderdtest.go | 1 + coderd/database/dbauthz/dbauthz.go | 22 +- coderd/database/dbauthz/dbauthz_test.go | 11 +- coderd/database/dbmem/dbmem.go | 46 +- coderd/database/dbmetrics/dbmetrics.go | 14 + coderd/database/dbmock/dbmock.go | 29 + coderd/database/querier.go | 2 + coderd/database/queries.sql.go | 21 + coderd/database/queries/siteconfig.sql | 7 + coderd/workspaceagents.go | 19 +- coderd/workspaceagents_test.go | 152 +++ coderd/workspaceapps/token.go | 4 + codersdk/workspacesdk/connector.go | 82 +- .../workspacesdk/connector_internal_test.go | 239 ++++- codersdk/workspacesdk/workspacesdk.go | 6 +- enterprise/coderd/coderd.go | 1 + enterprise/tailnet/workspaceproxy.go | 1 + .../wsproxy/wsproxysdk/wsproxysdk_test.go | 1 + tailnet/configmaps.go | 10 + tailnet/conn.go | 4 + tailnet/coordinator_test.go | 2 + tailnet/proto/tailnet.pb.go | 947 ++++++++++-------- tailnet/proto/tailnet.proto | 9 + tailnet/proto/tailnet_drpc.pb.go | 42 +- tailnet/resume.go | 117 +++ tailnet/service.go | 16 + tailnet/service_test.go | 2 + tailnet/test/integration/integration.go | 1 + 30 files changed, 1428 insertions(+), 432 deletions(-) create mode 100644 tailnet/resume.go diff --git a/cli/server.go b/cli/server.go index c8c1e9232bbe2..33db837f2640b 100644 --- a/cli/server.go +++ b/cli/server.go @@ -791,18 +791,52 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } } - keyBytes, err := hex.DecodeString(oauthSigningKeyStr) + oauthKeyBytes, err := hex.DecodeString(oauthSigningKeyStr) if err != nil { return xerrors.Errorf("decode oauth signing key from database: %w", err) } - if len(keyBytes) != len(options.OAuthSigningKey) { - return xerrors.Errorf("oauth signing key in database is not the correct length, expect %d got %d", len(options.OAuthSigningKey), len(keyBytes)) + if len(oauthKeyBytes) != len(options.OAuthSigningKey) { + return xerrors.Errorf("oauth signing key in database is not the correct length, expect %d got %d", len(options.OAuthSigningKey), len(oauthKeyBytes)) } - copy(options.OAuthSigningKey[:], keyBytes) + copy(options.OAuthSigningKey[:], oauthKeyBytes) if options.OAuthSigningKey == [32]byte{} { return xerrors.Errorf("oauth signing key in database is empty") } + // Read the coordinator resume token signing key from the + // database. + resumeTokenKey := [64]byte{} + resumeTokenKeyStr, err := tx.GetCoordinatorResumeTokenSigningKey(ctx) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + return xerrors.Errorf("get coordinator resume token key: %w", err) + } + if decoded, err := hex.DecodeString(resumeTokenKeyStr); err != nil || len(decoded) != len(resumeTokenKey) { + b := make([]byte, len(resumeTokenKey)) + _, err := rand.Read(b) + if err != nil { + return xerrors.Errorf("generate fresh coordinator resume token key: %w", err) + } + + resumeTokenKeyStr = hex.EncodeToString(b) + err = tx.UpsertCoordinatorResumeTokenSigningKey(ctx, resumeTokenKeyStr) + if err != nil { + return xerrors.Errorf("insert freshly generated coordinator resume token key to database: %w", err) + } + } + + resumeTokenKeyBytes, err := hex.DecodeString(resumeTokenKeyStr) + if err != nil { + return xerrors.Errorf("decode coordinator resume token key from database: %w", err) + } + if len(resumeTokenKeyBytes) != len(resumeTokenKey) { + return xerrors.Errorf("coordinator resume token key in database is not the correct length, expect %d got %d", len(resumeTokenKey), len(resumeTokenKeyBytes)) + } + copy(resumeTokenKey[:], resumeTokenKeyBytes) + if resumeTokenKey == [64]byte{} { + return xerrors.Errorf("coordinator resume token key in database is empty") + } + options.CoordinatorResumeTokenProvider = tailnet.NewResumeTokenKeyProvider(resumeTokenKey, tailnet.DefaultResumeTokenExpiry) + return nil }, nil) if err != nil { diff --git a/coderd/coderd.go b/coderd/coderd.go index 896918f1c6d76..9c61540708a66 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -182,6 +182,9 @@ type Options struct { // AppSecurityKey is the crypto key used to sign and encrypt tokens related to // workspace applications. It consists of both a signing and encryption key. AppSecurityKey workspaceapps.SecurityKey + // CoordinatorResumeTokenProvider is used to provide and validate resume + // tokens issued by and passed to the coordinator DRPC API. + CoordinatorResumeTokenProvider tailnet.ResumeTokenProvider HealthcheckFunc func(ctx context.Context, apiKey string) *healthsdk.HealthcheckReport HealthcheckTimeout time.Duration @@ -583,12 +586,16 @@ func New(options *Options) *API { api.Options.NetworkTelemetryBatchMaxSize, api.handleNetworkTelemetry, ) + if options.CoordinatorResumeTokenProvider == nil { + panic("CoordinatorResumeTokenProvider is nil") + } api.TailnetClientService, err = tailnet.NewClientService(tailnet.ClientServiceOptions{ Logger: api.Logger.Named("tailnetclient"), CoordPtr: &api.TailnetCoordinator, DERPMapUpdateFrequency: api.Options.DERPMapUpdateFrequency, DERPMapFn: api.DERPMap, NetworkTelemetryHandler: api.NetworkTelemetryBatcher.Handler, + ResumeTokenProvider: api.Options.CoordinatorResumeTokenProvider, }) if err != nil { api.Logger.Fatal(api.ctx, "failed to initialize tailnet client service", slog.Error(err)) @@ -613,6 +620,9 @@ func New(options *Options) *API { options.WorkspaceAppsStatsCollectorOptions.Reporter = api.statsReporter } + if options.AppSecurityKey.IsZero() { + api.Logger.Fatal(api.ctx, "app security key cannot be zero") + } api.workspaceAppServer = &workspaceapps.Server{ Logger: workspaceAppsLogger, diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index f2353cdf5fc5b..ceeeb75465c89 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -492,6 +492,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can TailnetCoordinator: options.Coordinator, BaseDERPMap: derpMap, DERPMapUpdateFrequency: 150 * time.Millisecond, + CoordinatorResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval, AgentStatsRefreshInterval: options.AgentStatsRefreshInterval, DeploymentValues: options.DeploymentValues, diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 1ab1183985098..37e20e2dd7ccd 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1252,7 +1252,9 @@ func (q *querier) GetAnnouncementBanners(ctx context.Context) (string, error) { } func (q *querier) GetAppSecurityKey(ctx context.Context) (string, error) { - // No authz checks + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return "", err + } return q.db.GetAppSecurityKey(ctx) } @@ -1284,6 +1286,13 @@ func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUI return q.db.GetAuthorizationUserRoles(ctx, userID) } +func (q *querier) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + return "", err + } + return q.db.GetCoordinatorResumeTokenSigningKey(ctx) +} + func (q *querier) GetDBCryptKeys(ctx context.Context) ([]database.DBCryptKey, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err @@ -3645,7 +3654,9 @@ func (q *querier) UpsertAnnouncementBanners(ctx context.Context, value string) e } func (q *querier) UpsertAppSecurityKey(ctx context.Context, data string) error { - // No authz checks as this is done during startup + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + return err + } return q.db.UpsertAppSecurityKey(ctx, data) } @@ -3656,6 +3667,13 @@ func (q *querier) UpsertApplicationName(ctx context.Context, value string) error return q.db.UpsertApplicationName(ctx, value) } +func (q *querier) UpsertCoordinatorResumeTokenSigningKey(ctx context.Context, value string) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + return err + } + return q.db.UpsertCoordinatorResumeTokenSigningKey(ctx, value) +} + // UpsertCustomRole does a series of authz checks to protect custom roles. // - Check custom roles are valid for their resource types + actions // - Check the actor can create the custom role diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 4eb764fde57b1..9c68d5f3f12bd 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2445,10 +2445,10 @@ func (s *MethodTestSuite) TestSystemFunctions() { check.Args(int32(0)).Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("GetAppSecurityKey", s.Subtest(func(db database.Store, check *expects) { - check.Args().Asserts() + check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("UpsertAppSecurityKey", s.Subtest(func(db database.Store, check *expects) { - check.Args("").Asserts() + check.Args("foo").Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) s.Run("GetApplicationName", s.Subtest(func(db database.Store, check *expects) { db.UpsertApplicationName(context.Background(), "foo") @@ -2488,6 +2488,13 @@ func (s *MethodTestSuite) TestSystemFunctions() { db.UpsertOAuthSigningKey(context.Background(), "foo") check.Args().Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) + s.Run("UpsertCoordinatorResumeTokenSigningKey", s.Subtest(func(db database.Store, check *expects) { + check.Args("foo").Asserts(rbac.ResourceSystem, policy.ActionUpdate) + })) + s.Run("GetCoordinatorResumeTokenSigningKey", s.Subtest(func(db database.Store, check *expects) { + db.UpsertCoordinatorResumeTokenSigningKey(context.Background(), "foo") + check.Args().Asserts(rbac.ResourceSystem, policy.ActionUpdate) + })) s.Run("InsertMissingGroups", s.Subtest(func(db database.Store, check *expects) { check.Args(database.InsertMissingGroupsParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate).Errors(errMatchAny) })) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 2ad54acd21473..6a475c0eca630 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -196,20 +196,21 @@ type data struct { customRoles []database.CustomRole // Locks is a map of lock names. Any keys within the map are currently // locked. - locks map[int64]struct{} - deploymentID string - derpMeshKey string - lastUpdateCheck []byte - announcementBanners []byte - healthSettings []byte - notificationsSettings []byte - applicationName string - logoURL string - appSecurityKey string - oauthSigningKey string - lastLicenseID int32 - defaultProxyDisplayName string - defaultProxyIconURL string + locks map[int64]struct{} + deploymentID string + derpMeshKey string + lastUpdateCheck []byte + announcementBanners []byte + healthSettings []byte + notificationsSettings []byte + applicationName string + logoURL string + appSecurityKey string + oauthSigningKey string + coordinatorResumeTokenSigningKey string + lastLicenseID int32 + defaultProxyDisplayName string + defaultProxyIconURL string } func validateDatabaseTypeWithValid(v reflect.Value) (handled bool, err error) { @@ -2172,6 +2173,15 @@ func (q *FakeQuerier) GetAuthorizationUserRoles(_ context.Context, userID uuid.U }, nil } +func (q *FakeQuerier) GetCoordinatorResumeTokenSigningKey(_ context.Context) (string, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + if q.coordinatorResumeTokenSigningKey == "" { + return "", sql.ErrNoRows + } + return q.coordinatorResumeTokenSigningKey, nil +} + func (q *FakeQuerier) GetDBCryptKeys(_ context.Context) ([]database.DBCryptKey, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -8800,6 +8810,14 @@ func (q *FakeQuerier) UpsertApplicationName(_ context.Context, data string) erro return nil } +func (q *FakeQuerier) UpsertCoordinatorResumeTokenSigningKey(_ context.Context, value string) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + q.coordinatorResumeTokenSigningKey = value + return nil +} + func (q *FakeQuerier) UpsertCustomRole(_ context.Context, arg database.UpsertCustomRoleParams) (database.CustomRole, error) { err := validateDatabaseType(arg) if err != nil { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 207f02d241e99..a759f55b20ffe 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -529,6 +529,13 @@ func (m metricsStore) GetAuthorizationUserRoles(ctx context.Context, userID uuid return row, err } +func (m metricsStore) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) { + start := time.Now() + r0, r1 := m.s.GetCoordinatorResumeTokenSigningKey(ctx) + m.queryLatencies.WithLabelValues("GetCoordinatorResumeTokenSigningKey").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetDBCryptKeys(ctx context.Context) ([]database.DBCryptKey, error) { start := time.Now() r0, r1 := m.s.GetDBCryptKeys(ctx) @@ -2363,6 +2370,13 @@ func (m metricsStore) UpsertApplicationName(ctx context.Context, value string) e return r0 } +func (m metricsStore) UpsertCoordinatorResumeTokenSigningKey(ctx context.Context, value string) error { + start := time.Now() + r0 := m.s.UpsertCoordinatorResumeTokenSigningKey(ctx, value) + m.queryLatencies.WithLabelValues("UpsertCoordinatorResumeTokenSigningKey").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) UpsertCustomRole(ctx context.Context, arg database.UpsertCustomRoleParams) (database.CustomRole, error) { start := time.Now() r0, r1 := m.s.UpsertCustomRole(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 11c1dcd86c831..2cdfb989b3bcd 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1029,6 +1029,21 @@ func (mr *MockStoreMockRecorder) GetAuthorizedWorkspaces(arg0, arg1, arg2 any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspaces", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspaces), arg0, arg1, arg2) } +// GetCoordinatorResumeTokenSigningKey mocks base method. +func (m *MockStore) GetCoordinatorResumeTokenSigningKey(arg0 context.Context) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCoordinatorResumeTokenSigningKey", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCoordinatorResumeTokenSigningKey indicates an expected call of GetCoordinatorResumeTokenSigningKey. +func (mr *MockStoreMockRecorder) GetCoordinatorResumeTokenSigningKey(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCoordinatorResumeTokenSigningKey", reflect.TypeOf((*MockStore)(nil).GetCoordinatorResumeTokenSigningKey), arg0) +} + // GetDBCryptKeys mocks base method. func (m *MockStore) GetDBCryptKeys(arg0 context.Context) ([]database.DBCryptKey, error) { m.ctrl.T.Helper() @@ -4965,6 +4980,20 @@ func (mr *MockStoreMockRecorder) UpsertApplicationName(arg0, arg1 any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertApplicationName", reflect.TypeOf((*MockStore)(nil).UpsertApplicationName), arg0, arg1) } +// UpsertCoordinatorResumeTokenSigningKey mocks base method. +func (m *MockStore) UpsertCoordinatorResumeTokenSigningKey(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertCoordinatorResumeTokenSigningKey", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertCoordinatorResumeTokenSigningKey indicates an expected call of UpsertCoordinatorResumeTokenSigningKey. +func (mr *MockStoreMockRecorder) UpsertCoordinatorResumeTokenSigningKey(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertCoordinatorResumeTokenSigningKey", reflect.TypeOf((*MockStore)(nil).UpsertCoordinatorResumeTokenSigningKey), arg0, arg1) +} + // UpsertCustomRole mocks base method. func (m *MockStore) UpsertCustomRole(arg0 context.Context, arg1 database.UpsertCustomRoleParams) (database.CustomRole, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 9feb5266cc2a2..0d91bf43b7d8f 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -128,6 +128,7 @@ type sqlcQuerier interface { // This function returns roles for authorization purposes. Implied member roles // are included. GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) + GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) GetDBCryptKeys(ctx context.Context) ([]DBCryptKey, error) GetDERPMeshKey(ctx context.Context) (string, error) GetDefaultOrganization(ctx context.Context) (Organization, error) @@ -460,6 +461,7 @@ type sqlcQuerier interface { UpsertAnnouncementBanners(ctx context.Context, value string) error UpsertAppSecurityKey(ctx context.Context, value string) error UpsertApplicationName(ctx context.Context, value string) error + UpsertCoordinatorResumeTokenSigningKey(ctx context.Context, value string) error UpsertCustomRole(ctx context.Context, arg UpsertCustomRoleParams) (CustomRole, error) // The default proxy is implied and not actually stored in the database. // So we need to store it's configuration here for display purposes. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ba8129584ccda..e64d2ca1ef984 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6638,6 +6638,17 @@ func (q *sqlQuerier) GetApplicationName(ctx context.Context) (string, error) { return value, err } +const getCoordinatorResumeTokenSigningKey = `-- name: GetCoordinatorResumeTokenSigningKey :one +SELECT value FROM site_configs WHERE key = 'coordinator_resume_token_signing_key' +` + +func (q *sqlQuerier) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) { + row := q.db.QueryRowContext(ctx, getCoordinatorResumeTokenSigningKey) + var value string + err := row.Scan(&value) + return value, err +} + const getDERPMeshKey = `-- name: GetDERPMeshKey :one SELECT value FROM site_configs WHERE key = 'derp_mesh_key' ` @@ -6783,6 +6794,16 @@ func (q *sqlQuerier) UpsertApplicationName(ctx context.Context, value string) er return err } +const upsertCoordinatorResumeTokenSigningKey = `-- name: UpsertCoordinatorResumeTokenSigningKey :exec +INSERT INTO site_configs (key, value) VALUES ('coordinator_resume_token_signing_key', $1) +ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'coordinator_resume_token_signing_key' +` + +func (q *sqlQuerier) UpsertCoordinatorResumeTokenSigningKey(ctx context.Context, value string) error { + _, err := q.db.ExecContext(ctx, upsertCoordinatorResumeTokenSigningKey, value) + return err +} + const upsertDefaultProxy = `-- name: UpsertDefaultProxy :exec INSERT INTO site_configs (key, value) VALUES diff --git a/coderd/database/queries/siteconfig.sql b/coderd/database/queries/siteconfig.sql index 9287a4aee0b54..877f5ee237122 100644 --- a/coderd/database/queries/siteconfig.sql +++ b/coderd/database/queries/siteconfig.sql @@ -71,6 +71,13 @@ SELECT value FROM site_configs WHERE key = 'oauth_signing_key'; INSERT INTO site_configs (key, value) VALUES ('oauth_signing_key', $1) ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'oauth_signing_key'; +-- name: GetCoordinatorResumeTokenSigningKey :one +SELECT value FROM site_configs WHERE key = 'coordinator_resume_token_signing_key'; + +-- name: UpsertCoordinatorResumeTokenSigningKey :exec +INSERT INTO site_configs (key, value) VALUES ('coordinator_resume_token_signing_key', $1) +ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'coordinator_resume_token_signing_key'; + -- name: GetHealthSettings :one SELECT COALESCE((SELECT value FROM site_configs WHERE key = 'health_settings'), '{}') :: text AS health_settings diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index e9e2ab18027d9..3bfd057ffb181 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -846,6 +846,23 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R return } + // Accept a resume_token query parameter to use the same peer ID. + var ( + peerID = uuid.New() + resumeToken = r.URL.Query().Get("resume_token") + ) + if resumeToken != "" { + var err error + peerID, err = api.Options.CoordinatorResumeTokenProvider.ParseResumeToken(resumeToken) + if err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: workspacesdk.CoordinateAPIInvalidResumeToken, + Detail: err.Error(), + }) + return + } + } + api.WebsocketWaitMutex.Lock() api.WebsocketWaitGroup.Add(1) api.WebsocketWaitMutex.Unlock() @@ -866,7 +883,7 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R go httpapi.Heartbeat(ctx, conn) defer conn.Close(websocket.StatusNormalClosure, "") - err = api.TailnetClientService.ServeClient(ctx, version, wsNetConn, uuid.New(), workspaceAgent.ID) + err = api.TailnetClientService.ServeClient(ctx, version, wsNetConn, peerID, workspaceAgent.ID) if err != nil && !xerrors.Is(err, io.EOF) && !xerrors.Is(err, context.Canceled) { _ = conn.Close(websocket.StatusInternalError, err.Error()) return diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 12d1d591fd46d..80e40b36f0a56 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -6,9 +6,12 @@ import ( "fmt" "net" "net/http" + "net/http/httptest" + "net/url" "runtime" "strconv" "strings" + "sync" "sync/atomic" "testing" "time" @@ -509,6 +512,118 @@ func TestWorkspaceAgentClientCoordinate_BadVersion(t *testing.T) { require.Equal(t, "version", sdkErr.Validations[0].Field) } +func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { + t.Parallel() + + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + + // We block DERP in this test to ensure that even if there's no direct + // connection, no shenanigans happen with the peer IDs on either side. + dv := coderdtest.DeploymentValues(t) + err := dv.DERP.Config.BlockDirect.Set("true") + require.NoError(t, err) + client, closer, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ + DeploymentValues: dv, + }) + defer closer.Close() + user := coderdtest.CreateFirstUser(t, client) + + // Change the DERP mapper to our custom one. + var currentDerpMap atomic.Pointer[tailcfg.DERPMap] + originalDerpMap, _ := tailnettest.RunDERPAndSTUN(t) + currentDerpMap.Store(originalDerpMap) + derpMapFn := func(_ *tailcfg.DERPMap) *tailcfg.DERPMap { + return currentDerpMap.Load().Clone() + } + api.DERPMapper.Store(&derpMapFn) + + // Start workspace a workspace agent. + r := dbfake.WorkspaceBuild(t, api.Database, database.Workspace{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + + agentCloser := agenttest.New(t, client.URL, r.AgentToken) + resources := coderdtest.AwaitWorkspaceAgents(t, client, r.Workspace.ID) + agentID := resources[0].Agents[0].ID + + // Create a new "proxy" server that we can use to kill the connection + // whenever we want. + l, err := netListenDroppable("tcp", "localhost:0") + require.NoError(t, err) + defer l.Close() + srv := &httptest.Server{ + Listener: l, + //nolint:gosec + Config: &http.Server{Handler: api.RootHandler}, + } + srv.Start() + proxyURL, err := url.Parse(srv.URL) + require.NoError(t, err) + proxyClient := codersdk.New(proxyURL) + proxyClient.SetSessionToken(client.SessionToken()) + + // Connect from a client. + conn, err := func() (*workspacesdk.AgentConn, error) { + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() // Connection should remain open even if the dial context is canceled. + + return workspacesdk.New(proxyClient). + DialAgent(ctx, agentID, &workspacesdk.DialAgentOptions{ + Logger: logger.Named("client"), + }) + }() + require.NoError(t, err) + defer conn.Close() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + ok := conn.AwaitReachable(ctx) + require.True(t, ok) + originalAgentPeers := agentCloser.TailnetConn().GetKnownPeerIDs() + + // Drop client conn's coordinator connection. + l.DropAllConns() + + // HACK: Change the DERP map and add a second "marker" region so we know + // when the client has reconnected to the coordinator. + // + // With some refactoring of the client connection to expose the + // coordinator connection status, this wouldn't be needed, but this + // also works. + derpMap := currentDerpMap.Load().Clone() + newDerpMap, _ := tailnettest.RunDERPAndSTUN(t) + derpMap.Regions[2] = newDerpMap.Regions[1] + currentDerpMap.Store(derpMap) + + // Wait for the agent's DERP map to be updated. + require.Eventually(t, func() bool { + conn := agentCloser.TailnetConn() + if conn == nil { + return false + } + regionIDs := conn.DERPMap().RegionIDs() + return len(regionIDs) == 2 && regionIDs[1] == 2 + }, testutil.WaitLong, testutil.IntervalFast) + + // Wait for the DERP map to be updated on the client. This means that the + // client has reconnected to the coordinator. + require.Eventually(t, func() bool { + regionIDs := conn.Conn.DERPMap().RegionIDs() + return len(regionIDs) == 2 && regionIDs[1] == 2 + }, testutil.WaitLong, testutil.IntervalFast) + + // The first client should still be able to reach the agent. + ok = conn.AwaitReachable(ctx) + require.True(t, ok) + _, err = conn.ListeningPorts(ctx) + require.NoError(t, err) + + // The agent should not see any new peers. + require.ElementsMatch(t, originalAgentPeers, agentCloser.TailnetConn().GetKnownPeerIDs()) +} + func TestWorkspaceAgentTailnetDirectDisabled(t *testing.T) { t.Parallel() @@ -1722,3 +1837,40 @@ func postStartup(ctx context.Context, t testing.TB, client agent.Client, startup _, err = aAPI.UpdateStartup(ctx, &agentproto.UpdateStartupRequest{Startup: startup}) return err } + +type droppableTCPListener struct { + net.Listener + mu sync.Mutex + conns []net.Conn +} + +var _ net.Listener = &droppableTCPListener{} + +func netListenDroppable(network, addr string) (*droppableTCPListener, error) { + l, err := net.Listen(network, addr) + if err != nil { + return nil, err + } + return &droppableTCPListener{Listener: l}, nil +} + +func (l *droppableTCPListener) Accept() (net.Conn, error) { + conn, err := l.Listener.Accept() + if err != nil { + return nil, err + } + + l.mu.Lock() + defer l.mu.Unlock() + l.conns = append(l.conns, conn) + return conn, nil +} + +func (l *droppableTCPListener) DropAllConns() { + l.mu.Lock() + defer l.mu.Unlock() + for _, c := range l.conns { + _ = c.Close() + } + l.conns = nil +} diff --git a/coderd/workspaceapps/token.go b/coderd/workspaceapps/token.go index 80423beab14d7..33428b0e25f13 100644 --- a/coderd/workspaceapps/token.go +++ b/coderd/workspaceapps/token.go @@ -65,6 +65,10 @@ func (t SignedToken) MatchesRequest(req Request) bool { // two keys. type SecurityKey [96]byte +func (k SecurityKey) IsZero() bool { + return k == SecurityKey{} +} + func (k SecurityKey) String() string { return hex.EncodeToString(k[:]) } diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index 5e5f528af6888..e897036a4bfdb 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "slices" "strings" "sync" @@ -68,9 +69,10 @@ type tailnetAPIConnector struct { clientMu sync.RWMutex client proto.DRPCTailnetClient - connected chan error - isFirst bool - closed chan struct{} + connected chan error + resumeToken atomic.Pointer[proto.RefreshResumeTokenResponse] + isFirst bool + closed chan struct{} // Only set to true if we get a response from the server that it doesn't support // network telemetry. @@ -121,7 +123,7 @@ func (tac *tailnetAPIConnector) runConnector(conn tailnetConn) { tac.client = tailnetClient tac.clientMu.Unlock() tac.logger.Debug(tac.ctx, "obtained tailnet API v2+ client") - tac.coordinateAndDERPMap(tailnetClient) + tac.runConnectorOnce(tailnetClient) tac.logger.Debug(tac.ctx, "tailnet API v2+ connection lost") } }() @@ -138,8 +140,24 @@ func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { return tac.customDialFn() } tac.logger.Debug(tac.ctx, "dialing Coder tailnet v2+ API") + + u, err := url.Parse(tac.coordinateURL) + if err != nil { + return nil, xerrors.Errorf("parse URL %q: %w", tac.coordinateURL, err) + } + resumeToken := tac.resumeToken.Load() + if resumeToken != nil { + q := u.Query() + q.Set("resume_token", resumeToken.Token) + u.RawQuery = q.Encode() + tac.logger.Debug(tac.ctx, "using resume token", slog.F("resume_token", resumeToken)) + } + + coordinateURL := u.String() + tac.logger.Debug(tac.ctx, "using coordinate URL", slog.F("url", coordinateURL)) + // nolint:bodyclose - ws, res, err := websocket.Dial(tac.ctx, tac.coordinateURL, tac.dialOptions) + ws, res, err := websocket.Dial(tac.ctx, coordinateURL, tac.dialOptions) if tac.isFirst { if res != nil && slices.Contains(permanentErrorStatuses, res.StatusCode) { err = codersdk.ReadBodyAsError(res) @@ -163,6 +181,17 @@ func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { if !errors.Is(err, context.Canceled) { tac.logger.Error(tac.ctx, "failed to dial tailnet v2+ API", slog.Error(err)) } + if res.StatusCode == http.StatusBadRequest { + err = codersdk.ReadBodyAsError(res) + var sdkErr *codersdk.Error + if xerrors.As(err, &sdkErr) { + if sdkErr.Message == CoordinateAPIInvalidResumeToken { + // Unset the resume token for the next attempt + tac.logger.Debug(tac.ctx, "server replied invalid resume token; unsetting for next connection attempt") + tac.resumeToken.Store(nil) + } + } + } return nil, err } client, err := tailnet.NewDRPCClient( @@ -177,11 +206,11 @@ func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { return client, err } -// coordinateAndDERPMap uses the provided client to coordinate and stream DERP Maps. It is combined +// runConnectorOnce uses the provided client to coordinate and stream DERP Maps. It is combined // into one function so that a problem with one tears down the other and triggers a retry (if // appropriate). We multiplex both RPCs over the same websocket, so we want them to share the same // fate. -func (tac *tailnetAPIConnector) coordinateAndDERPMap(client proto.DRPCTailnetClient) { +func (tac *tailnetAPIConnector) runConnectorOnce(client proto.DRPCTailnetClient) { defer func() { conn := client.DRPCConn() closeErr := conn.Close() @@ -193,14 +222,17 @@ func (tac *tailnetAPIConnector) coordinateAndDERPMap(client proto.DRPCTailnetCli <-conn.Closed() } }() + + refreshTokenCtx, refreshTokenCancel := context.WithCancel(tac.ctx) wg := sync.WaitGroup{} - wg.Add(2) + wg.Add(3) go func() { defer wg.Done() tac.coordinate(client) }() go func() { defer wg.Done() + defer refreshTokenCancel() dErr := tac.derpMap(client) if dErr != nil && tac.ctx.Err() == nil { // The main context is still active, meaning that we want the tailnet data plane to stay @@ -215,6 +247,10 @@ func (tac *tailnetAPIConnector) coordinateAndDERPMap(client proto.DRPCTailnetCli // Note that derpMap() logs it own errors, we don't bother here. } }() + go func() { + defer wg.Done() + tac.refreshToken(refreshTokenCtx, client) + }() wg.Wait() } @@ -278,6 +314,36 @@ func (tac *tailnetAPIConnector) derpMap(client proto.DRPCTailnetClient) error { } } +func (tac *tailnetAPIConnector) refreshToken(ctx context.Context, client proto.DRPCTailnetClient) { + ticker := time.NewTicker(15 * time.Second) + defer ticker.Stop() + + initialCh := make(chan struct{}, 1) + initialCh <- struct{}{} + defer close(initialCh) + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + case <-initialCh: + } + + attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + res, err := client.RefreshResumeToken(attemptCtx, &proto.RefreshResumeTokenRequest{}) + cancel() + if err != nil { + if ctx.Err() == nil { + tac.logger.Error(tac.ctx, "error refreshing coordinator resume token", slog.Error(err)) + } + return + } + tac.logger.Debug(tac.ctx, "refreshed coordinator resume token", slog.F("resume_token", res)) + tac.resumeToken.Store(res) + ticker.Reset(res.RefreshIn.AsDuration()) + } +} + func (tac *tailnetAPIConnector) SendTelemetryEvent(event *proto.TelemetryEvent) { tac.clientMu.RLock() // We hold the lock for the entire telemetry request, but this would only block diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index 0106c271b68a4..3b200c20b10cb 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -14,6 +14,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" "nhooyr.io/websocket" "storj.io/drpc" "storj.io/drpc/drpcerr" @@ -59,6 +61,7 @@ func TestTailnetAPIConnector_Disconnects(t *testing.T) { DERPMapUpdateFrequency: time.Millisecond, DERPMapFn: func() *tailcfg.DERPMap { return <-derpMapCh }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) {}, + ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, }) require.NoError(t, err) @@ -142,6 +145,225 @@ func TestTailnetAPIConnector_UplevelVersion(t *testing.T) { require.NotEmpty(t, sdkErr.Helper) } +func TestTailnetAPIConnector_ResumeToken(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, &slogtest.Options{ + IgnoreErrors: true, + }).Leveled(slog.LevelDebug) + agentID := uuid.UUID{0x55} + fCoord := tailnettest.NewFakeCoordinator() + var coord tailnet.Coordinator = fCoord + coordPtr := atomic.Pointer[tailnet.Coordinator]{} + coordPtr.Store(&coord) + derpMapCh := make(chan *tailcfg.DERPMap) + defer close(derpMapCh) + + resumeTokenProvider := tailnet.NewResumeTokenKeyProvider([64]byte{1}, time.Second) + svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ + Logger: logger, + CoordPtr: &coordPtr, + DERPMapUpdateFrequency: time.Millisecond, + DERPMapFn: func() *tailcfg.DERPMap { return <-derpMapCh }, + NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) {}, + ResumeTokenProvider: resumeTokenProvider, + }) + require.NoError(t, err) + + var ( + websocketConnCh = make(chan *websocket.Conn, 64) + peerIDCh = make(chan uuid.UUID, 64) + expectResumeToken = "" + ) + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Accept a resume_token query parameter to use the same peer ID. This + // behavior matches the actual client coordinate route. + var ( + peerID = uuid.New() + resumeToken = r.URL.Query().Get("resume_token") + ) + t.Logf("received resume token: %s", resumeToken) + assert.Equal(t, expectResumeToken, resumeToken) + if resumeToken != "" { + peerID, err = resumeTokenProvider.ParseResumeToken(resumeToken) + assert.NoError(t, err, "failed to parse resume token") + if err != nil { + httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ + Message: CoordinateAPIInvalidResumeToken, + Detail: err.Error(), + }) + return + } + } + testutil.RequireSendCtx(ctx, t, peerIDCh, peerID) + + sws, err := websocket.Accept(w, r, nil) + if !assert.NoError(t, err) { + return + } + testutil.RequireSendCtx(ctx, t, websocketConnCh, sws) + ctx, nc := codersdk.WebsocketNetConn(r.Context(), sws, websocket.MessageBinary) + err = svc.ServeConnV2(ctx, nc, tailnet.StreamID{ + Name: "client", + ID: peerID, + Auth: tailnet.ClientCoordinateeAuth{AgentID: agentID}, + }) + assert.NoError(t, err) + })) + + fConn := newFakeTailnetConn() + + uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}) + uut.runConnector(fConn) + + // Wait for the resume token to be fetched for the first time. + require.Eventually(t, func() bool { + return uut.resumeToken.Load() != nil + }, testutil.WaitShort, testutil.IntervalFast) + originalResumeToken := uut.resumeToken.Load() + require.NotNil(t, originalResumeToken) + expectResumeToken = originalResumeToken.Token + t.Logf("expecting resume token: %s", expectResumeToken) + + // Sever the connection and expect it to reconnect with the resume token and + // assume the same peer ID. + originalPeerID := testutil.RequireRecvCtx(ctx, t, peerIDCh) + wsConn := testutil.RequireRecvCtx(ctx, t, websocketConnCh) + _ = wsConn.Close(websocket.StatusGoingAway, "test") + + // Wait for the resume token to be refreshed. + require.Eventually(t, func() bool { + rt := uut.resumeToken.Load() + return rt != nil && rt.Token != originalResumeToken.Token + }, testutil.WaitShort, testutil.IntervalFast) + + // Peer ID should be identical. + require.Equal(t, originalPeerID, testutil.RequireRecvCtx(ctx, t, peerIDCh)) +} + +type resumeTokenProvider struct { + genFn func(uuid.UUID) (*proto.RefreshResumeTokenResponse, error) + parseFn func(string) (uuid.UUID, error) +} + +var _ tailnet.ResumeTokenProvider = resumeTokenProvider{} + +// GenerateResumeToken implements tailnet.ResumeTokenProvider. +func (r resumeTokenProvider) GenerateResumeToken(peerID uuid.UUID) (*proto.RefreshResumeTokenResponse, error) { + return r.genFn(peerID) +} + +// ParseResumeToken implements tailnet.ResumeTokenProvider. +func (r resumeTokenProvider) ParseResumeToken(token string) (uuid.UUID, error) { + return r.parseFn(token) +} + +func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, &slogtest.Options{ + IgnoreErrors: true, + }).Leveled(slog.LevelDebug) + agentID := uuid.UUID{0x55} + fCoord := tailnettest.NewFakeCoordinator() + var coord tailnet.Coordinator = fCoord + coordPtr := atomic.Pointer[tailnet.Coordinator]{} + coordPtr.Store(&coord) + derpMapCh := make(chan *tailcfg.DERPMap) + defer close(derpMapCh) + + resumeTokenProvider := resumeTokenProvider{ + genFn: func(uuid.UUID) (*proto.RefreshResumeTokenResponse, error) { + return &proto.RefreshResumeTokenResponse{ + Token: uuid.NewString(), + RefreshIn: durationpb.New(time.Minute), + ExpiresAt: timestamppb.New(time.Now().Add(time.Hour)), + }, nil + }, + parseFn: func(string) (uuid.UUID, error) { + return uuid.UUID{}, xerrors.New("test error") + }, + } + svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ + Logger: logger, + CoordPtr: &coordPtr, + DERPMapUpdateFrequency: time.Millisecond, + DERPMapFn: func() *tailcfg.DERPMap { return <-derpMapCh }, + NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) {}, + ResumeTokenProvider: resumeTokenProvider, + }) + require.NoError(t, err) + + var ( + websocketConnCh = make(chan *websocket.Conn, 64) + peerIDCh = make(chan uuid.UUID, 64) + didFail int64 + ) + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Accept a resume_token query parameter to use the same peer ID. + var ( + peerID = uuid.New() + resumeToken = r.URL.Query().Get("resume_token") + ) + t.Logf("received resume token: %s", resumeToken) + if resumeToken != "" { + _, err = resumeTokenProvider.ParseResumeToken(resumeToken) + assert.Error(t, err, "parse resume token should return an error") + atomic.AddInt64(&didFail, 1) + httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ + Message: CoordinateAPIInvalidResumeToken, + Detail: err.Error(), + }) + return + } + testutil.RequireSendCtx(ctx, t, peerIDCh, peerID) + + sws, err := websocket.Accept(w, r, nil) + if !assert.NoError(t, err) { + return + } + testutil.RequireSendCtx(ctx, t, websocketConnCh, sws) + ctx, nc := codersdk.WebsocketNetConn(r.Context(), sws, websocket.MessageBinary) + err = svc.ServeConnV2(ctx, nc, tailnet.StreamID{ + Name: "client", + ID: peerID, + Auth: tailnet.ClientCoordinateeAuth{AgentID: agentID}, + }) + assert.NoError(t, err) + })) + + fConn := newFakeTailnetConn() + + uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}) + uut.runConnector(fConn) + + // Wait for the resume token to be fetched for the first time. + require.Eventually(t, func() bool { + return uut.resumeToken.Load() != nil + }, testutil.WaitShort, testutil.IntervalFast) + originalResumeToken := uut.resumeToken.Load() + require.NotNil(t, originalResumeToken) + + // Sever the connection and expect it to reconnect with the resume token, + // which should fail and cause the client to be disconnected. The client + // should then reconnect with no resume token. + originalPeerID := testutil.RequireRecvCtx(ctx, t, peerIDCh) + wsConn := testutil.RequireRecvCtx(ctx, t, websocketConnCh) + _ = wsConn.Close(websocket.StatusGoingAway, "test") + + // Wait for the resume token to be refreshed. + require.Eventually(t, func() bool { + rt := uut.resumeToken.Load() + return rt != nil && rt.Token != originalResumeToken.Token + }, testutil.WaitShort, testutil.IntervalFast) + + // Peer ID should be different. + require.NotEqual(t, originalPeerID, testutil.RequireRecvCtx(ctx, t, peerIDCh)) + + // The resume token should have failed to parse. + require.EqualValues(t, 1, atomic.LoadInt64(&didFail)) +} + func TestTailnetAPIConnector_TelemetrySuccess(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) @@ -161,8 +383,9 @@ func TestTailnetAPIConnector_TelemetrySuccess(t *testing.T) { DERPMapUpdateFrequency: time.Millisecond, DERPMapFn: func() *tailcfg.DERPMap { return <-derpMapCh }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { - eventCh <- batch + testutil.RequireSendCtx(ctx, t, eventCh, batch) }, + ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, }) require.NoError(t, err) @@ -301,6 +524,7 @@ func newFakeTailnetConn() *fakeTailnetConn { type fakeDRPCClient struct { postTelemetryCalls int64 + refreshTokenFn func(context.Context, *proto.RefreshResumeTokenRequest) (*proto.RefreshResumeTokenResponse, error) telemetryError error fakeDRPPCMapStream } @@ -339,6 +563,19 @@ func (f *fakeDRPCClient) StreamDERPMaps(_ context.Context, _ *proto.StreamDERPMa return &f.fakeDRPPCMapStream, nil } +// RefreshResumeToken implements proto.DRPCTailnetClient. +func (f *fakeDRPCClient) RefreshResumeToken(_ context.Context, _ *proto.RefreshResumeTokenRequest) (*proto.RefreshResumeTokenResponse, error) { + if f.refreshTokenFn != nil { + return f.refreshTokenFn(context.Background(), nil) + } + + return &proto.RefreshResumeTokenResponse{ + Token: "test", + RefreshIn: durationpb.New(30 * time.Minute), + ExpiresAt: timestamppb.New(time.Now().Add(time.Hour)), + }, nil +} + type fakeDRPCConn struct{} var _ drpc.Conn = &fakeDRPCConn{} diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index a38ed1c05c91d..b0797c0cc789a 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -55,7 +55,11 @@ const ( AgentMinimumListeningPort = 9 ) -const AgentAPIMismatchMessage = "Unknown or unsupported API version" +const ( + AgentAPIMismatchMessage = "Unknown or unsupported API version" + + CoordinateAPIInvalidResumeToken = "Invalid resume token" +) // AgentIgnoredListeningPorts contains a list of ports to ignore when looking for // running applications inside a workspace. We want to ignore non-HTTP servers, diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 501e6086eaeb3..a4241b34dc4bc 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -146,6 +146,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { DERPMapUpdateFrequency: api.Options.DERPMapUpdateFrequency, DERPMapFn: api.AGPL.DERPMap, NetworkTelemetryHandler: api.AGPL.NetworkTelemetryBatcher.Handler, + ResumeTokenProvider: api.AGPL.CoordinatorResumeTokenProvider, }) if err != nil { api.Logger.Fatal(api.ctx, "failed to initialize tailnet client service", slog.Error(err)) diff --git a/enterprise/tailnet/workspaceproxy.go b/enterprise/tailnet/workspaceproxy.go index 674536755434f..c786be72a99f6 100644 --- a/enterprise/tailnet/workspaceproxy.go +++ b/enterprise/tailnet/workspaceproxy.go @@ -30,6 +30,7 @@ func NewClientService(options agpl.ClientServiceOptions) (*ClientService, error) DERPMapUpdateFrequency: options.DERPMapUpdateFrequency, DERPMapFn: options.DERPMapFn, NetworkTelemetryHandler: options.NetworkTelemetryHandler, + ResumeTokenProvider: options.ResumeTokenProvider, }) if err != nil { return nil, err diff --git a/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go b/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go index 1ed49881092fb..456f046d7a148 100644 --- a/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go +++ b/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go @@ -177,6 +177,7 @@ func TestDialCoordinator(t *testing.T) { DERPMapUpdateFrequency: time.Hour, DERPMapFn: func() *tailcfg.DERPMap { panic("not implemented") }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { panic("not implemented") }, + ResumeTokenProvider: agpl.InsecureTestResumeTokenProvider, }) require.NoError(t, err) diff --git a/tailnet/configmaps.go b/tailnet/configmaps.go index a6ef9f40028b1..3af711ffa8f3a 100644 --- a/tailnet/configmaps.go +++ b/tailnet/configmaps.go @@ -608,6 +608,16 @@ func (c *configMaps) fillPeerDiagnostics(d *PeerDiagnostics, peerID uuid.UUID) { d.LastWireguardHandshake = ps.LastHandshake } +func (c *configMaps) knownPeerIDs() []uuid.UUID { + c.L.Lock() + defer c.L.Unlock() + out := make([]uuid.UUID, 0, len(c.peers)) + for id := range c.peers { + out = append(out, id) + } + return out +} + func (c *configMaps) peerReadyForHandshakeTimeout(peerID uuid.UUID) { logger := c.logger.With(slog.F("peer_id", peerID)) logger.Debug(context.Background(), "peer ready for handshake timeout") diff --git a/tailnet/conn.go b/tailnet/conn.go index 4646c2f02cbeb..22b6d7287357b 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -847,6 +847,10 @@ func (c *Conn) GetPeerDiagnostics(peerID uuid.UUID) PeerDiagnostics { return d } +func (c *Conn) GetKnownPeerIDs() []uuid.UUID { + return c.configMaps.knownPeerIDs() +} + type listenKey struct { network string host string diff --git a/tailnet/coordinator_test.go b/tailnet/coordinator_test.go index cdf288c98ddb9..218fc9d3def2b 100644 --- a/tailnet/coordinator_test.go +++ b/tailnet/coordinator_test.go @@ -630,6 +630,7 @@ func TestRemoteCoordination(t *testing.T) { DERPMapUpdateFrequency: time.Hour, DERPMapFn: func() *tailcfg.DERPMap { panic("not implemented") }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { panic("not implemented") }, + ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, }) require.NoError(t, err) sC, cC := net.Pipe() @@ -681,6 +682,7 @@ func TestRemoteCoordination_SendsReadyForHandshake(t *testing.T) { DERPMapUpdateFrequency: time.Hour, DERPMapFn: func() *tailcfg.DERPMap { panic("not implemented") }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { panic("not implemented") }, + ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, }) require.NoError(t, err) sC, cC := net.Pipe() diff --git a/tailnet/proto/tailnet.pb.go b/tailnet/proto/tailnet.pb.go index c344fd3bca989..2e90da9d97ab9 100644 --- a/tailnet/proto/tailnet.pb.go +++ b/tailnet/proto/tailnet.pb.go @@ -75,7 +75,7 @@ func (x CoordinateResponse_PeerUpdate_Kind) Number() protoreflect.EnumNumber { // Deprecated: Use CoordinateResponse_PeerUpdate_Kind.Descriptor instead. func (CoordinateResponse_PeerUpdate_Kind) EnumDescriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{4, 0, 0} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6, 0, 0} } type IPFields_IPClass int32 @@ -127,7 +127,7 @@ func (x IPFields_IPClass) Number() protoreflect.EnumNumber { // Deprecated: Use IPFields_IPClass.Descriptor instead. func (IPFields_IPClass) EnumDescriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{5, 0} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7, 0} } type TelemetryEvent_Status int32 @@ -173,7 +173,7 @@ func (x TelemetryEvent_Status) Number() protoreflect.EnumNumber { // Deprecated: Use TelemetryEvent_Status.Descriptor instead. func (TelemetryEvent_Status) EnumDescriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7, 0} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{9, 0} } type TelemetryEvent_ClientType int32 @@ -225,7 +225,7 @@ func (x TelemetryEvent_ClientType) Number() protoreflect.EnumNumber { // Deprecated: Use TelemetryEvent_ClientType.Descriptor instead. func (TelemetryEvent_ClientType) EnumDescriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7, 1} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{9, 1} } type DERPMap struct { @@ -441,6 +441,107 @@ func (x *Node) GetEndpoints() []string { return nil } +type RefreshResumeTokenRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RefreshResumeTokenRequest) Reset() { + *x = RefreshResumeTokenRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RefreshResumeTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshResumeTokenRequest) ProtoMessage() {} + +func (x *RefreshResumeTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[3] + 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 RefreshResumeTokenRequest.ProtoReflect.Descriptor instead. +func (*RefreshResumeTokenRequest) Descriptor() ([]byte, []int) { + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{3} +} + +type RefreshResumeTokenResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + RefreshIn *durationpb.Duration `protobuf:"bytes,2,opt,name=refresh_in,json=refreshIn,proto3" json:"refresh_in,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` +} + +func (x *RefreshResumeTokenResponse) Reset() { + *x = RefreshResumeTokenResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RefreshResumeTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshResumeTokenResponse) ProtoMessage() {} + +func (x *RefreshResumeTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[4] + 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 RefreshResumeTokenResponse.ProtoReflect.Descriptor instead. +func (*RefreshResumeTokenResponse) Descriptor() ([]byte, []int) { + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{4} +} + +func (x *RefreshResumeTokenResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *RefreshResumeTokenResponse) GetRefreshIn() *durationpb.Duration { + if x != nil { + return x.RefreshIn + } + return nil +} + +func (x *RefreshResumeTokenResponse) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + type CoordinateRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -456,7 +557,7 @@ type CoordinateRequest struct { func (x *CoordinateRequest) Reset() { *x = CoordinateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[3] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -469,7 +570,7 @@ func (x *CoordinateRequest) String() string { func (*CoordinateRequest) ProtoMessage() {} func (x *CoordinateRequest) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[3] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -482,7 +583,7 @@ func (x *CoordinateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CoordinateRequest.ProtoReflect.Descriptor instead. func (*CoordinateRequest) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{3} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{5} } func (x *CoordinateRequest) GetUpdateSelf() *CoordinateRequest_UpdateSelf { @@ -532,7 +633,7 @@ type CoordinateResponse struct { func (x *CoordinateResponse) Reset() { *x = CoordinateResponse{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[4] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -545,7 +646,7 @@ func (x *CoordinateResponse) String() string { func (*CoordinateResponse) ProtoMessage() {} func (x *CoordinateResponse) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[4] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -558,7 +659,7 @@ func (x *CoordinateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CoordinateResponse.ProtoReflect.Descriptor instead. func (*CoordinateResponse) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{4} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6} } func (x *CoordinateResponse) GetPeerUpdates() []*CoordinateResponse_PeerUpdate { @@ -587,7 +688,7 @@ type IPFields struct { func (x *IPFields) Reset() { *x = IPFields{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[5] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -600,7 +701,7 @@ func (x *IPFields) String() string { func (*IPFields) ProtoMessage() {} func (x *IPFields) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[5] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -613,7 +714,7 @@ func (x *IPFields) ProtoReflect() protoreflect.Message { // Deprecated: Use IPFields.ProtoReflect.Descriptor instead. func (*IPFields) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{5} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7} } func (x *IPFields) GetVersion() int32 { @@ -657,7 +758,7 @@ type Netcheck struct { func (x *Netcheck) Reset() { *x = Netcheck{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[6] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -670,7 +771,7 @@ func (x *Netcheck) String() string { func (*Netcheck) ProtoMessage() {} func (x *Netcheck) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[6] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -683,7 +784,7 @@ func (x *Netcheck) ProtoReflect() protoreflect.Message { // Deprecated: Use Netcheck.ProtoReflect.Descriptor instead. func (*Netcheck) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{8} } func (x *Netcheck) GetUDP() bool { @@ -834,7 +935,7 @@ type TelemetryEvent struct { func (x *TelemetryEvent) Reset() { *x = TelemetryEvent{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[7] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -847,7 +948,7 @@ func (x *TelemetryEvent) String() string { func (*TelemetryEvent) ProtoMessage() {} func (x *TelemetryEvent) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[7] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -860,7 +961,7 @@ func (x *TelemetryEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use TelemetryEvent.ProtoReflect.Descriptor instead. func (*TelemetryEvent) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{9} } func (x *TelemetryEvent) GetId() []byte { @@ -1007,7 +1108,7 @@ type TelemetryRequest struct { func (x *TelemetryRequest) Reset() { *x = TelemetryRequest{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[8] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1020,7 +1121,7 @@ func (x *TelemetryRequest) String() string { func (*TelemetryRequest) ProtoMessage() {} func (x *TelemetryRequest) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[8] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1033,7 +1134,7 @@ func (x *TelemetryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TelemetryRequest.ProtoReflect.Descriptor instead. func (*TelemetryRequest) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{8} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{10} } func (x *TelemetryRequest) GetEvents() []*TelemetryEvent { @@ -1052,7 +1153,7 @@ type TelemetryResponse struct { func (x *TelemetryResponse) Reset() { *x = TelemetryResponse{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[9] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1065,7 +1166,7 @@ func (x *TelemetryResponse) String() string { func (*TelemetryResponse) ProtoMessage() {} func (x *TelemetryResponse) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[9] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1078,7 +1179,7 @@ func (x *TelemetryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TelemetryResponse.ProtoReflect.Descriptor instead. func (*TelemetryResponse) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{9} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{11} } type DERPMap_HomeParams struct { @@ -1092,7 +1193,7 @@ type DERPMap_HomeParams struct { func (x *DERPMap_HomeParams) Reset() { *x = DERPMap_HomeParams{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[10] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1105,7 +1206,7 @@ func (x *DERPMap_HomeParams) String() string { func (*DERPMap_HomeParams) ProtoMessage() {} func (x *DERPMap_HomeParams) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[10] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1144,7 +1245,7 @@ type DERPMap_Region struct { func (x *DERPMap_Region) Reset() { *x = DERPMap_Region{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[11] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1157,7 +1258,7 @@ func (x *DERPMap_Region) String() string { func (*DERPMap_Region) ProtoMessage() {} func (x *DERPMap_Region) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[11] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1238,7 +1339,7 @@ type DERPMap_Region_Node struct { func (x *DERPMap_Region_Node) Reset() { *x = DERPMap_Region_Node{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[14] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1251,7 +1352,7 @@ func (x *DERPMap_Region_Node) String() string { func (*DERPMap_Region_Node) ProtoMessage() {} func (x *DERPMap_Region_Node) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[14] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1369,7 +1470,7 @@ type CoordinateRequest_UpdateSelf struct { func (x *CoordinateRequest_UpdateSelf) Reset() { *x = CoordinateRequest_UpdateSelf{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[17] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1382,7 +1483,7 @@ func (x *CoordinateRequest_UpdateSelf) String() string { func (*CoordinateRequest_UpdateSelf) ProtoMessage() {} func (x *CoordinateRequest_UpdateSelf) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[17] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1395,7 +1496,7 @@ func (x *CoordinateRequest_UpdateSelf) ProtoReflect() protoreflect.Message { // Deprecated: Use CoordinateRequest_UpdateSelf.ProtoReflect.Descriptor instead. func (*CoordinateRequest_UpdateSelf) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{3, 0} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{5, 0} } func (x *CoordinateRequest_UpdateSelf) GetNode() *Node { @@ -1414,7 +1515,7 @@ type CoordinateRequest_Disconnect struct { func (x *CoordinateRequest_Disconnect) Reset() { *x = CoordinateRequest_Disconnect{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[18] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1427,7 +1528,7 @@ func (x *CoordinateRequest_Disconnect) String() string { func (*CoordinateRequest_Disconnect) ProtoMessage() {} func (x *CoordinateRequest_Disconnect) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[18] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1440,7 +1541,7 @@ func (x *CoordinateRequest_Disconnect) ProtoReflect() protoreflect.Message { // Deprecated: Use CoordinateRequest_Disconnect.ProtoReflect.Descriptor instead. func (*CoordinateRequest_Disconnect) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{3, 1} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{5, 1} } type CoordinateRequest_Tunnel struct { @@ -1454,7 +1555,7 @@ type CoordinateRequest_Tunnel struct { func (x *CoordinateRequest_Tunnel) Reset() { *x = CoordinateRequest_Tunnel{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[19] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1467,7 +1568,7 @@ func (x *CoordinateRequest_Tunnel) String() string { func (*CoordinateRequest_Tunnel) ProtoMessage() {} func (x *CoordinateRequest_Tunnel) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[19] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1480,7 +1581,7 @@ func (x *CoordinateRequest_Tunnel) ProtoReflect() protoreflect.Message { // Deprecated: Use CoordinateRequest_Tunnel.ProtoReflect.Descriptor instead. func (*CoordinateRequest_Tunnel) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{3, 2} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{5, 2} } func (x *CoordinateRequest_Tunnel) GetId() []byte { @@ -1505,7 +1606,7 @@ type CoordinateRequest_ReadyForHandshake struct { func (x *CoordinateRequest_ReadyForHandshake) Reset() { *x = CoordinateRequest_ReadyForHandshake{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1518,7 +1619,7 @@ func (x *CoordinateRequest_ReadyForHandshake) String() string { func (*CoordinateRequest_ReadyForHandshake) ProtoMessage() {} func (x *CoordinateRequest_ReadyForHandshake) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1531,7 +1632,7 @@ func (x *CoordinateRequest_ReadyForHandshake) ProtoReflect() protoreflect.Messag // Deprecated: Use CoordinateRequest_ReadyForHandshake.ProtoReflect.Descriptor instead. func (*CoordinateRequest_ReadyForHandshake) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{3, 3} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{5, 3} } func (x *CoordinateRequest_ReadyForHandshake) GetId() []byte { @@ -1555,7 +1656,7 @@ type CoordinateResponse_PeerUpdate struct { func (x *CoordinateResponse_PeerUpdate) Reset() { *x = CoordinateResponse_PeerUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[21] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1568,7 +1669,7 @@ func (x *CoordinateResponse_PeerUpdate) String() string { func (*CoordinateResponse_PeerUpdate) ProtoMessage() {} func (x *CoordinateResponse_PeerUpdate) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[21] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1581,7 +1682,7 @@ func (x *CoordinateResponse_PeerUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use CoordinateResponse_PeerUpdate.ProtoReflect.Descriptor instead. func (*CoordinateResponse_PeerUpdate) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{4, 0} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6, 0} } func (x *CoordinateResponse_PeerUpdate) GetId() []byte { @@ -1624,7 +1725,7 @@ type Netcheck_NetcheckIP struct { func (x *Netcheck_NetcheckIP) Reset() { *x = Netcheck_NetcheckIP{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[24] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1637,7 +1738,7 @@ func (x *Netcheck_NetcheckIP) String() string { func (*Netcheck_NetcheckIP) ProtoMessage() {} func (x *Netcheck_NetcheckIP) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[24] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1650,7 +1751,7 @@ func (x *Netcheck_NetcheckIP) ProtoReflect() protoreflect.Message { // Deprecated: Use Netcheck_NetcheckIP.ProtoReflect.Descriptor instead. func (*Netcheck_NetcheckIP) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6, 2} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{8, 2} } func (x *Netcheck_NetcheckIP) GetHash() string { @@ -1680,7 +1781,7 @@ type TelemetryEvent_P2PEndpoint struct { func (x *TelemetryEvent_P2PEndpoint) Reset() { *x = TelemetryEvent_P2PEndpoint{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[25] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1693,7 +1794,7 @@ func (x *TelemetryEvent_P2PEndpoint) String() string { func (*TelemetryEvent_P2PEndpoint) ProtoMessage() {} func (x *TelemetryEvent_P2PEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[25] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1706,7 +1807,7 @@ func (x *TelemetryEvent_P2PEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use TelemetryEvent_P2PEndpoint.ProtoReflect.Descriptor instead. func (*TelemetryEvent_P2PEndpoint) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7, 0} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{9, 0} } func (x *TelemetryEvent_P2PEndpoint) GetHash() string { @@ -1842,254 +1943,274 @@ var file_tailnet_proto_tailnet_proto_rawDesc = []byte{ 0x57, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xbe, 0x04, 0x0a, 0x11, 0x43, - 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x4f, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, - 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x65, 0x6c, 0x66, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x6c, - 0x66, 0x12, 0x4e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, - 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x12, 0x49, 0x0a, 0x0a, 0x61, 0x64, 0x64, 0x5f, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, - 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x75, 0x6e, 0x6e, 0x65, - 0x6c, 0x52, 0x09, 0x61, 0x64, 0x64, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x4f, 0x0a, 0x0d, - 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x5f, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, - 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x52, - 0x0c, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x65, 0x0a, - 0x13, 0x72, 0x65, 0x61, 0x64, 0x79, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x73, - 0x68, 0x61, 0x6b, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x63, 0x6f, 0x64, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1b, 0x0a, 0x19, 0x52, 0x65, + 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa7, 0x01, 0x0a, 0x1a, 0x52, 0x65, 0x66, 0x72, + 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x38, 0x0a, 0x0a, + 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x72, 0x65, 0x66, + 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x73, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, + 0x74, 0x22, 0xbe, 0x04, 0x0a, 0x11, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4f, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x6c, 0x66, 0x52, 0x0a, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x6c, 0x66, 0x12, 0x4e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x52, 0x0a, 0x64, 0x69, + 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x49, 0x0a, 0x0a, 0x61, 0x64, 0x64, 0x5f, + 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x2e, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x09, 0x61, 0x64, 0x64, 0x54, 0x75, 0x6e, + 0x6e, 0x65, 0x6c, 0x12, 0x4f, 0x0a, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x5f, 0x74, 0x75, + 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, - 0x52, 0x65, 0x61, 0x64, 0x79, 0x46, 0x6f, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, - 0x65, 0x52, 0x11, 0x72, 0x65, 0x61, 0x64, 0x79, 0x46, 0x6f, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, - 0x68, 0x61, 0x6b, 0x65, 0x1a, 0x38, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, - 0x6c, 0x66, 0x12, 0x2a, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x0c, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x75, + 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x65, 0x0a, 0x13, 0x72, 0x65, 0x61, 0x64, 0x79, 0x5f, 0x66, 0x6f, + 0x72, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x79, 0x46, 0x6f, 0x72, 0x48, + 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x11, 0x72, 0x65, 0x61, 0x64, 0x79, 0x46, + 0x6f, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x1a, 0x38, 0x0a, 0x0a, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x6c, 0x66, 0x12, 0x2a, 0x0a, 0x04, 0x6e, 0x6f, 0x64, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, + 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x1a, 0x0c, 0x0a, 0x0a, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x1a, 0x18, 0x0a, 0x06, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x1a, 0x23, 0x0a, + 0x11, 0x52, 0x65, 0x61, 0x64, 0x79, 0x46, 0x6f, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, + 0x6b, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, + 0x69, 0x64, 0x22, 0x88, 0x03, 0x0a, 0x12, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0c, 0x70, 0x65, 0x65, + 0x72, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x1a, 0x87, 0x02, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x2a, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x1a, 0x0c, - 0x0a, 0x0a, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x1a, 0x18, 0x0a, 0x06, - 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x1a, 0x23, 0x0a, 0x11, 0x52, 0x65, 0x61, 0x64, 0x79, 0x46, - 0x6f, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x22, 0x88, 0x03, 0x0a, 0x12, - 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, - 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x50, - 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x87, 0x02, 0x0a, - 0x0a, 0x50, 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2a, 0x0a, 0x04, 0x6e, - 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x6f, 0x64, - 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x48, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, - 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x50, 0x65, 0x65, 0x72, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x04, 0x6b, 0x69, 0x6e, - 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x5b, 0x0a, 0x04, 0x4b, 0x69, 0x6e, - 0x64, 0x12, 0x14, 0x0a, 0x10, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x44, 0x45, 0x10, - 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, - 0x44, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x4f, 0x53, 0x54, 0x10, 0x03, 0x12, 0x17, 0x0a, - 0x13, 0x52, 0x45, 0x41, 0x44, 0x59, 0x5f, 0x46, 0x4f, 0x52, 0x5f, 0x48, 0x41, 0x4e, 0x44, 0x53, - 0x48, 0x41, 0x4b, 0x45, 0x10, 0x04, 0x22, 0xa0, 0x01, 0x0a, 0x08, 0x49, 0x50, 0x46, 0x69, 0x65, - 0x6c, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, - 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x63, + 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x48, + 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x49, 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x2e, 0x49, 0x50, 0x43, 0x6c, 0x61, 0x73, 0x73, - 0x52, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x22, 0x40, 0x0a, 0x07, 0x49, 0x50, 0x43, 0x6c, 0x61, - 0x73, 0x73, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x00, 0x12, 0x0b, - 0x0a, 0x07, 0x50, 0x52, 0x49, 0x56, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, - 0x49, 0x4e, 0x4b, 0x5f, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x4c, - 0x4f, 0x4f, 0x50, 0x42, 0x41, 0x43, 0x4b, 0x10, 0x03, 0x22, 0xec, 0x08, 0x0a, 0x08, 0x4e, 0x65, - 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x03, 0x55, 0x44, 0x50, 0x12, 0x12, 0x0a, 0x04, 0x49, 0x50, 0x76, 0x36, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x49, 0x50, 0x76, 0x36, 0x12, 0x12, 0x0a, 0x04, - 0x49, 0x50, 0x76, 0x34, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x49, 0x50, 0x76, 0x34, - 0x12, 0x20, 0x0a, 0x0b, 0x49, 0x50, 0x76, 0x36, 0x43, 0x61, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x49, 0x50, 0x76, 0x36, 0x43, 0x61, 0x6e, 0x53, 0x65, - 0x6e, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x49, 0x50, 0x76, 0x34, 0x43, 0x61, 0x6e, 0x53, 0x65, 0x6e, - 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x49, 0x50, 0x76, 0x34, 0x43, 0x61, 0x6e, - 0x53, 0x65, 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x49, 0x43, 0x4d, 0x50, 0x76, 0x34, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x49, 0x43, 0x4d, 0x50, 0x76, 0x34, 0x12, 0x38, 0x0a, 0x09, - 0x4f, 0x53, 0x48, 0x61, 0x73, 0x49, 0x50, 0x76, 0x36, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x4f, 0x53, 0x48, - 0x61, 0x73, 0x49, 0x50, 0x76, 0x36, 0x12, 0x50, 0x0a, 0x15, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x56, 0x61, 0x72, 0x69, 0x65, 0x73, 0x42, 0x79, 0x44, 0x65, 0x73, 0x74, 0x49, 0x50, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x15, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x72, 0x69, 0x65, 0x73, - 0x42, 0x79, 0x44, 0x65, 0x73, 0x74, 0x49, 0x50, 0x12, 0x3c, 0x0a, 0x0b, 0x48, 0x61, 0x69, 0x72, - 0x50, 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0b, 0x48, 0x61, 0x69, 0x72, 0x50, - 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x2e, 0x0a, 0x04, 0x55, 0x50, 0x6e, 0x50, 0x18, 0x0a, + 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x4b, 0x69, + 0x6e, 0x64, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x22, 0x5b, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a, 0x10, 0x4b, 0x49, 0x4e, 0x44, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, + 0x0a, 0x04, 0x4e, 0x4f, 0x44, 0x45, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x44, 0x49, 0x53, 0x43, + 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x4f, + 0x53, 0x54, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x41, 0x44, 0x59, 0x5f, 0x46, 0x4f, + 0x52, 0x5f, 0x48, 0x41, 0x4e, 0x44, 0x53, 0x48, 0x41, 0x4b, 0x45, 0x10, 0x04, 0x22, 0xa0, 0x01, + 0x0a, 0x08, 0x49, 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, + 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x49, 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x2e, + 0x49, 0x50, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x52, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x22, 0x40, + 0x0a, 0x07, 0x49, 0x50, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, + 0x4c, 0x49, 0x43, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x49, 0x56, 0x41, 0x54, 0x45, + 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, 0x49, 0x4e, 0x4b, 0x5f, 0x4c, 0x4f, 0x43, 0x41, 0x4c, + 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x4f, 0x50, 0x42, 0x41, 0x43, 0x4b, 0x10, 0x03, + 0x22, 0xec, 0x08, 0x0a, 0x08, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, + 0x03, 0x55, 0x44, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x55, 0x44, 0x50, 0x12, + 0x12, 0x0a, 0x04, 0x49, 0x50, 0x76, 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x49, + 0x50, 0x76, 0x36, 0x12, 0x12, 0x0a, 0x04, 0x49, 0x50, 0x76, 0x34, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x04, 0x49, 0x50, 0x76, 0x34, 0x12, 0x20, 0x0a, 0x0b, 0x49, 0x50, 0x76, 0x36, 0x43, + 0x61, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x49, 0x50, + 0x76, 0x36, 0x43, 0x61, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x49, 0x50, 0x76, + 0x34, 0x43, 0x61, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, + 0x49, 0x50, 0x76, 0x34, 0x43, 0x61, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x49, + 0x43, 0x4d, 0x50, 0x76, 0x34, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x49, 0x43, 0x4d, + 0x50, 0x76, 0x34, 0x12, 0x38, 0x0a, 0x09, 0x4f, 0x53, 0x48, 0x61, 0x73, 0x49, 0x50, 0x76, 0x36, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x09, 0x4f, 0x53, 0x48, 0x61, 0x73, 0x49, 0x50, 0x76, 0x36, 0x12, 0x50, 0x0a, + 0x15, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x72, 0x69, 0x65, 0x73, 0x42, 0x79, + 0x44, 0x65, 0x73, 0x74, 0x49, 0x50, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, + 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x15, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x56, 0x61, 0x72, 0x69, 0x65, 0x73, 0x42, 0x79, 0x44, 0x65, 0x73, 0x74, 0x49, 0x50, 0x12, + 0x3c, 0x0a, 0x0b, 0x48, 0x61, 0x69, 0x72, 0x50, 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x04, 0x55, 0x50, 0x6e, 0x50, 0x12, 0x2c, 0x0a, 0x03, 0x50, 0x4d, 0x50, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x03, 0x50, 0x4d, 0x50, 0x12, 0x2c, 0x0a, 0x03, 0x50, 0x43, 0x50, 0x18, 0x0c, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x03, 0x50, - 0x43, 0x50, 0x12, 0x24, 0x0a, 0x0d, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x44, - 0x45, 0x52, 0x50, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x50, 0x72, 0x65, 0x66, 0x65, - 0x72, 0x72, 0x65, 0x64, 0x44, 0x45, 0x52, 0x50, 0x12, 0x59, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, - 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x0e, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x52, 0x65, - 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, - 0x6e, 0x63, 0x79, 0x12, 0x59, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, - 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, - 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x52, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x41, - 0x0a, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x34, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x4e, 0x65, 0x74, - 0x63, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x50, 0x52, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, - 0x34, 0x12, 0x41, 0x0a, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x36, 0x18, 0x12, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, + 0x52, 0x0b, 0x48, 0x61, 0x69, 0x72, 0x50, 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x2e, 0x0a, + 0x04, 0x55, 0x50, 0x6e, 0x50, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, + 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x04, 0x55, 0x50, 0x6e, 0x50, 0x12, 0x2c, 0x0a, + 0x03, 0x50, 0x4d, 0x50, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, + 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x03, 0x50, 0x4d, 0x50, 0x12, 0x2c, 0x0a, 0x03, 0x50, + 0x43, 0x50, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x03, 0x50, 0x43, 0x50, 0x12, 0x24, 0x0a, 0x0d, 0x50, 0x72, 0x65, + 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x44, 0x45, 0x52, 0x50, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0d, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x44, 0x45, 0x52, 0x50, 0x12, + 0x59, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, + 0x63, 0x79, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, + 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x59, 0x0a, 0x0f, 0x52, 0x65, + 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x0f, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x2e, - 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x50, 0x52, 0x08, 0x47, 0x6c, 0x6f, 0x62, - 0x61, 0x6c, 0x56, 0x36, 0x1a, 0x5d, 0x0a, 0x14, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, - 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x1a, 0x5d, 0x0a, 0x14, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, - 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x1a, 0x54, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x50, - 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x68, 0x61, 0x73, 0x68, 0x12, 0x32, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, - 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x49, 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, - 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, 0xdf, 0x09, 0x0a, 0x0e, 0x54, 0x65, 0x6c, - 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x61, - 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3f, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, + 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, + 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x41, 0x0a, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, + 0x34, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, + 0x65, 0x63, 0x6b, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x50, 0x52, 0x08, + 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x34, 0x12, 0x41, 0x0a, 0x08, 0x47, 0x6c, 0x6f, 0x62, + 0x61, 0x6c, 0x56, 0x36, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, + 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x49, + 0x50, 0x52, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x36, 0x1a, 0x5d, 0x0a, 0x14, 0x52, + 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5d, 0x0a, 0x14, 0x52, 0x65, + 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x54, 0x0a, 0x0a, 0x4e, 0x65, 0x74, + 0x63, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x50, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x32, 0x0a, 0x06, 0x66, + 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x49, + 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, + 0xdf, 0x09, 0x0a, 0x0e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, + 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, + 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, + 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x31, 0x0a, 0x14, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x13, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x31, - 0x0a, 0x14, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x64, 0x69, - 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, - 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, - 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, - 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, - 0x79, 0x70, 0x65, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, - 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6e, 0x6f, - 0x64, 0x65, 0x49, 0x64, 0x53, 0x65, 0x6c, 0x66, 0x12, 0x24, 0x0a, 0x0e, 0x6e, 0x6f, 0x64, 0x65, - 0x5f, 0x69, 0x64, 0x5f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x12, 0x4f, - 0x0a, 0x0c, 0x70, 0x32, 0x70, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, - 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, - 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x32, 0x50, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x52, 0x0b, 0x70, 0x32, 0x70, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, - 0x1b, 0x0a, 0x09, 0x68, 0x6f, 0x6d, 0x65, 0x5f, 0x64, 0x65, 0x72, 0x70, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x08, 0x68, 0x6f, 0x6d, 0x65, 0x44, 0x65, 0x72, 0x70, 0x12, 0x34, 0x0a, 0x08, - 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x52, 0x07, 0x64, 0x65, 0x72, 0x70, 0x4d, - 0x61, 0x70, 0x12, 0x43, 0x0a, 0x0f, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x6e, 0x65, 0x74, - 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, - 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0e, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x4e, - 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x40, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x67, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x67, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x63, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x75, 0x70, 0x18, 0x0e, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, - 0x36, 0x0a, 0x09, 0x70, 0x32, 0x70, 0x5f, 0x73, 0x65, 0x74, 0x75, 0x70, 0x18, 0x0f, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x70, - 0x32, 0x70, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x3c, 0x0a, 0x0c, 0x64, 0x65, 0x72, 0x70, 0x5f, - 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x72, 0x70, 0x4c, 0x61, - 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x3a, 0x0a, 0x0b, 0x70, 0x32, 0x70, 0x5f, 0x6c, 0x61, 0x74, - 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, + 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x53, 0x65, 0x6c, 0x66, 0x12, + 0x24, 0x0a, 0x0e, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x72, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x52, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x70, 0x32, 0x70, 0x5f, 0x65, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, + 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x32, + 0x50, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0b, 0x70, 0x32, 0x70, 0x45, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x68, 0x6f, 0x6d, 0x65, 0x5f, 0x64, + 0x65, 0x72, 0x70, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x68, 0x6f, 0x6d, 0x65, 0x44, + 0x65, 0x72, 0x70, 0x12, 0x34, 0x0a, 0x08, 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6d, 0x61, 0x70, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, + 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, + 0x52, 0x07, 0x64, 0x65, 0x72, 0x70, 0x4d, 0x61, 0x70, 0x12, 0x43, 0x0a, 0x0f, 0x6c, 0x61, 0x74, + 0x65, 0x73, 0x74, 0x5f, 0x6e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, + 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0e, + 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x40, + 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x67, 0x65, + 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x67, 0x65, + 0x12, 0x44, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, + 0x65, 0x74, 0x75, 0x70, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x32, 0x70, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, - 0x79, 0x12, 0x46, 0x0a, 0x10, 0x74, 0x68, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x70, 0x75, 0x74, 0x5f, - 0x6d, 0x62, 0x69, 0x74, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x6c, - 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x74, 0x68, 0x72, 0x6f, 0x75, 0x67, - 0x68, 0x70, 0x75, 0x74, 0x4d, 0x62, 0x69, 0x74, 0x73, 0x1a, 0x69, 0x0a, 0x0b, 0x50, 0x32, 0x50, - 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, - 0x12, 0x32, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x49, 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x52, 0x06, 0x66, 0x69, - 0x65, 0x6c, 0x64, 0x73, 0x22, 0x29, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0d, - 0x0a, 0x09, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, - 0x0c, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x01, 0x22, - 0x39, 0x0a, 0x0a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, - 0x03, 0x43, 0x4c, 0x49, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x47, 0x45, 0x4e, 0x54, 0x10, - 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x4f, 0x44, 0x45, 0x52, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, - 0x07, 0x57, 0x53, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x10, 0x03, 0x22, 0x4c, 0x0a, 0x10, 0x54, 0x65, - 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, - 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x13, 0x0a, 0x11, 0x54, 0x65, 0x6c, 0x65, - 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x98, 0x02, - 0x0a, 0x07, 0x54, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x12, 0x58, 0x0a, 0x0d, 0x50, 0x6f, 0x73, - 0x74, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, - 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x45, 0x52, - 0x50, 0x4d, 0x61, 0x70, 0x73, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, - 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, - 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x30, 0x01, 0x12, 0x5b, 0x0a, 0x0a, 0x43, - 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, - 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x29, 0x5a, 0x27, 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, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x36, 0x0a, 0x09, 0x70, 0x32, 0x70, 0x5f, 0x73, 0x65, + 0x74, 0x75, 0x70, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x70, 0x32, 0x70, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x3c, + 0x0a, 0x0c, 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x10, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x0b, 0x64, 0x65, 0x72, 0x70, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x3a, 0x0a, 0x0b, + 0x70, 0x32, 0x70, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x32, + 0x70, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x46, 0x0a, 0x10, 0x74, 0x68, 0x72, 0x6f, + 0x75, 0x67, 0x68, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x62, 0x69, 0x74, 0x73, 0x18, 0x12, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x0f, 0x74, 0x68, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x70, 0x75, 0x74, 0x4d, 0x62, 0x69, 0x74, 0x73, + 0x1a, 0x69, 0x0a, 0x0b, 0x50, 0x32, 0x50, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, + 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x49, 0x50, 0x46, 0x69, 0x65, + 0x6c, 0x64, 0x73, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, 0x29, 0x0a, 0x06, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, + 0x43, 0x54, 0x45, 0x44, 0x10, 0x01, 0x22, 0x39, 0x0a, 0x0a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x43, 0x4c, 0x49, 0x10, 0x00, 0x12, 0x09, 0x0a, + 0x05, 0x41, 0x47, 0x45, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x4f, 0x44, 0x45, + 0x52, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x53, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x10, + 0x03, 0x22, 0x4c, 0x0a, 0x10, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, + 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, + 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x22, + 0x13, 0x0a, 0x11, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x89, 0x03, 0x0a, 0x07, 0x54, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, + 0x12, 0x58, 0x0a, 0x0d, 0x50, 0x6f, 0x73, 0x74, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, + 0x79, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, + 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, + 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x0e, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x73, 0x12, 0x27, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, + 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, + 0x30, 0x01, 0x12, 0x6f, 0x0a, 0x12, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, + 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x66, 0x72, + 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, + 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, + 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, + 0x65, 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, + 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, + 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, + 0x42, 0x29, 0x5a, 0x27, 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, 0x74, 0x61, + 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -2105,100 +2226,106 @@ func file_tailnet_proto_tailnet_proto_rawDescGZIP() []byte { } var file_tailnet_proto_tailnet_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_tailnet_proto_tailnet_proto_msgTypes = make([]protoimpl.MessageInfo, 26) +var file_tailnet_proto_tailnet_proto_msgTypes = make([]protoimpl.MessageInfo, 28) var file_tailnet_proto_tailnet_proto_goTypes = []interface{}{ - (CoordinateResponse_PeerUpdate_Kind)(0), // 0: coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind - (IPFields_IPClass)(0), // 1: coder.tailnet.v2.IPFields.IPClass - (TelemetryEvent_Status)(0), // 2: coder.tailnet.v2.TelemetryEvent.Status - (TelemetryEvent_ClientType)(0), // 3: coder.tailnet.v2.TelemetryEvent.ClientType - (*DERPMap)(nil), // 4: coder.tailnet.v2.DERPMap - (*StreamDERPMapsRequest)(nil), // 5: coder.tailnet.v2.StreamDERPMapsRequest - (*Node)(nil), // 6: coder.tailnet.v2.Node - (*CoordinateRequest)(nil), // 7: coder.tailnet.v2.CoordinateRequest - (*CoordinateResponse)(nil), // 8: coder.tailnet.v2.CoordinateResponse - (*IPFields)(nil), // 9: coder.tailnet.v2.IPFields - (*Netcheck)(nil), // 10: coder.tailnet.v2.Netcheck - (*TelemetryEvent)(nil), // 11: coder.tailnet.v2.TelemetryEvent - (*TelemetryRequest)(nil), // 12: coder.tailnet.v2.TelemetryRequest - (*TelemetryResponse)(nil), // 13: coder.tailnet.v2.TelemetryResponse - (*DERPMap_HomeParams)(nil), // 14: coder.tailnet.v2.DERPMap.HomeParams - (*DERPMap_Region)(nil), // 15: coder.tailnet.v2.DERPMap.Region - nil, // 16: coder.tailnet.v2.DERPMap.RegionsEntry - nil, // 17: coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry - (*DERPMap_Region_Node)(nil), // 18: coder.tailnet.v2.DERPMap.Region.Node - nil, // 19: coder.tailnet.v2.Node.DerpLatencyEntry - nil, // 20: coder.tailnet.v2.Node.DerpForcedWebsocketEntry - (*CoordinateRequest_UpdateSelf)(nil), // 21: coder.tailnet.v2.CoordinateRequest.UpdateSelf - (*CoordinateRequest_Disconnect)(nil), // 22: coder.tailnet.v2.CoordinateRequest.Disconnect - (*CoordinateRequest_Tunnel)(nil), // 23: coder.tailnet.v2.CoordinateRequest.Tunnel - (*CoordinateRequest_ReadyForHandshake)(nil), // 24: coder.tailnet.v2.CoordinateRequest.ReadyForHandshake - (*CoordinateResponse_PeerUpdate)(nil), // 25: coder.tailnet.v2.CoordinateResponse.PeerUpdate - nil, // 26: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry - nil, // 27: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry - (*Netcheck_NetcheckIP)(nil), // 28: coder.tailnet.v2.Netcheck.NetcheckIP - (*TelemetryEvent_P2PEndpoint)(nil), // 29: coder.tailnet.v2.TelemetryEvent.P2PEndpoint - (*timestamppb.Timestamp)(nil), // 30: google.protobuf.Timestamp - (*wrapperspb.BoolValue)(nil), // 31: google.protobuf.BoolValue - (*durationpb.Duration)(nil), // 32: google.protobuf.Duration - (*wrapperspb.FloatValue)(nil), // 33: google.protobuf.FloatValue + (CoordinateResponse_PeerUpdate_Kind)(0), // 0: coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind + (IPFields_IPClass)(0), // 1: coder.tailnet.v2.IPFields.IPClass + (TelemetryEvent_Status)(0), // 2: coder.tailnet.v2.TelemetryEvent.Status + (TelemetryEvent_ClientType)(0), // 3: coder.tailnet.v2.TelemetryEvent.ClientType + (*DERPMap)(nil), // 4: coder.tailnet.v2.DERPMap + (*StreamDERPMapsRequest)(nil), // 5: coder.tailnet.v2.StreamDERPMapsRequest + (*Node)(nil), // 6: coder.tailnet.v2.Node + (*RefreshResumeTokenRequest)(nil), // 7: coder.tailnet.v2.RefreshResumeTokenRequest + (*RefreshResumeTokenResponse)(nil), // 8: coder.tailnet.v2.RefreshResumeTokenResponse + (*CoordinateRequest)(nil), // 9: coder.tailnet.v2.CoordinateRequest + (*CoordinateResponse)(nil), // 10: coder.tailnet.v2.CoordinateResponse + (*IPFields)(nil), // 11: coder.tailnet.v2.IPFields + (*Netcheck)(nil), // 12: coder.tailnet.v2.Netcheck + (*TelemetryEvent)(nil), // 13: coder.tailnet.v2.TelemetryEvent + (*TelemetryRequest)(nil), // 14: coder.tailnet.v2.TelemetryRequest + (*TelemetryResponse)(nil), // 15: coder.tailnet.v2.TelemetryResponse + (*DERPMap_HomeParams)(nil), // 16: coder.tailnet.v2.DERPMap.HomeParams + (*DERPMap_Region)(nil), // 17: coder.tailnet.v2.DERPMap.Region + nil, // 18: coder.tailnet.v2.DERPMap.RegionsEntry + nil, // 19: coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry + (*DERPMap_Region_Node)(nil), // 20: coder.tailnet.v2.DERPMap.Region.Node + nil, // 21: coder.tailnet.v2.Node.DerpLatencyEntry + nil, // 22: coder.tailnet.v2.Node.DerpForcedWebsocketEntry + (*CoordinateRequest_UpdateSelf)(nil), // 23: coder.tailnet.v2.CoordinateRequest.UpdateSelf + (*CoordinateRequest_Disconnect)(nil), // 24: coder.tailnet.v2.CoordinateRequest.Disconnect + (*CoordinateRequest_Tunnel)(nil), // 25: coder.tailnet.v2.CoordinateRequest.Tunnel + (*CoordinateRequest_ReadyForHandshake)(nil), // 26: coder.tailnet.v2.CoordinateRequest.ReadyForHandshake + (*CoordinateResponse_PeerUpdate)(nil), // 27: coder.tailnet.v2.CoordinateResponse.PeerUpdate + nil, // 28: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry + nil, // 29: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry + (*Netcheck_NetcheckIP)(nil), // 30: coder.tailnet.v2.Netcheck.NetcheckIP + (*TelemetryEvent_P2PEndpoint)(nil), // 31: coder.tailnet.v2.TelemetryEvent.P2PEndpoint + (*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 33: google.protobuf.Duration + (*wrapperspb.BoolValue)(nil), // 34: google.protobuf.BoolValue + (*wrapperspb.FloatValue)(nil), // 35: google.protobuf.FloatValue } var file_tailnet_proto_tailnet_proto_depIdxs = []int32{ - 14, // 0: coder.tailnet.v2.DERPMap.home_params:type_name -> coder.tailnet.v2.DERPMap.HomeParams - 16, // 1: coder.tailnet.v2.DERPMap.regions:type_name -> coder.tailnet.v2.DERPMap.RegionsEntry - 30, // 2: coder.tailnet.v2.Node.as_of:type_name -> google.protobuf.Timestamp - 19, // 3: coder.tailnet.v2.Node.derp_latency:type_name -> coder.tailnet.v2.Node.DerpLatencyEntry - 20, // 4: coder.tailnet.v2.Node.derp_forced_websocket:type_name -> coder.tailnet.v2.Node.DerpForcedWebsocketEntry - 21, // 5: coder.tailnet.v2.CoordinateRequest.update_self:type_name -> coder.tailnet.v2.CoordinateRequest.UpdateSelf - 22, // 6: coder.tailnet.v2.CoordinateRequest.disconnect:type_name -> coder.tailnet.v2.CoordinateRequest.Disconnect - 23, // 7: coder.tailnet.v2.CoordinateRequest.add_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel - 23, // 8: coder.tailnet.v2.CoordinateRequest.remove_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel - 24, // 9: coder.tailnet.v2.CoordinateRequest.ready_for_handshake:type_name -> coder.tailnet.v2.CoordinateRequest.ReadyForHandshake - 25, // 10: coder.tailnet.v2.CoordinateResponse.peer_updates:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate - 1, // 11: coder.tailnet.v2.IPFields.class:type_name -> coder.tailnet.v2.IPFields.IPClass - 31, // 12: coder.tailnet.v2.Netcheck.OSHasIPv6:type_name -> google.protobuf.BoolValue - 31, // 13: coder.tailnet.v2.Netcheck.MappingVariesByDestIP:type_name -> google.protobuf.BoolValue - 31, // 14: coder.tailnet.v2.Netcheck.HairPinning:type_name -> google.protobuf.BoolValue - 31, // 15: coder.tailnet.v2.Netcheck.UPnP:type_name -> google.protobuf.BoolValue - 31, // 16: coder.tailnet.v2.Netcheck.PMP:type_name -> google.protobuf.BoolValue - 31, // 17: coder.tailnet.v2.Netcheck.PCP:type_name -> google.protobuf.BoolValue - 26, // 18: coder.tailnet.v2.Netcheck.RegionV4Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV4LatencyEntry - 27, // 19: coder.tailnet.v2.Netcheck.RegionV6Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV6LatencyEntry - 28, // 20: coder.tailnet.v2.Netcheck.GlobalV4:type_name -> coder.tailnet.v2.Netcheck.NetcheckIP - 28, // 21: coder.tailnet.v2.Netcheck.GlobalV6:type_name -> coder.tailnet.v2.Netcheck.NetcheckIP - 30, // 22: coder.tailnet.v2.TelemetryEvent.time:type_name -> google.protobuf.Timestamp - 2, // 23: coder.tailnet.v2.TelemetryEvent.status:type_name -> coder.tailnet.v2.TelemetryEvent.Status - 3, // 24: coder.tailnet.v2.TelemetryEvent.client_type:type_name -> coder.tailnet.v2.TelemetryEvent.ClientType - 29, // 25: coder.tailnet.v2.TelemetryEvent.p2p_endpoint:type_name -> coder.tailnet.v2.TelemetryEvent.P2PEndpoint - 4, // 26: coder.tailnet.v2.TelemetryEvent.derp_map:type_name -> coder.tailnet.v2.DERPMap - 10, // 27: coder.tailnet.v2.TelemetryEvent.latest_netcheck:type_name -> coder.tailnet.v2.Netcheck - 32, // 28: coder.tailnet.v2.TelemetryEvent.connection_age:type_name -> google.protobuf.Duration - 32, // 29: coder.tailnet.v2.TelemetryEvent.connection_setup:type_name -> google.protobuf.Duration - 32, // 30: coder.tailnet.v2.TelemetryEvent.p2p_setup:type_name -> google.protobuf.Duration - 32, // 31: coder.tailnet.v2.TelemetryEvent.derp_latency:type_name -> google.protobuf.Duration - 32, // 32: coder.tailnet.v2.TelemetryEvent.p2p_latency:type_name -> google.protobuf.Duration - 33, // 33: coder.tailnet.v2.TelemetryEvent.throughput_mbits:type_name -> google.protobuf.FloatValue - 11, // 34: coder.tailnet.v2.TelemetryRequest.events:type_name -> coder.tailnet.v2.TelemetryEvent - 17, // 35: coder.tailnet.v2.DERPMap.HomeParams.region_score:type_name -> coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry - 18, // 36: coder.tailnet.v2.DERPMap.Region.nodes:type_name -> coder.tailnet.v2.DERPMap.Region.Node - 15, // 37: coder.tailnet.v2.DERPMap.RegionsEntry.value:type_name -> coder.tailnet.v2.DERPMap.Region - 6, // 38: coder.tailnet.v2.CoordinateRequest.UpdateSelf.node:type_name -> coder.tailnet.v2.Node - 6, // 39: coder.tailnet.v2.CoordinateResponse.PeerUpdate.node:type_name -> coder.tailnet.v2.Node - 0, // 40: coder.tailnet.v2.CoordinateResponse.PeerUpdate.kind:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind - 32, // 41: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry.value:type_name -> google.protobuf.Duration - 32, // 42: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry.value:type_name -> google.protobuf.Duration - 9, // 43: coder.tailnet.v2.Netcheck.NetcheckIP.fields:type_name -> coder.tailnet.v2.IPFields - 9, // 44: coder.tailnet.v2.TelemetryEvent.P2PEndpoint.fields:type_name -> coder.tailnet.v2.IPFields - 12, // 45: coder.tailnet.v2.Tailnet.PostTelemetry:input_type -> coder.tailnet.v2.TelemetryRequest - 5, // 46: coder.tailnet.v2.Tailnet.StreamDERPMaps:input_type -> coder.tailnet.v2.StreamDERPMapsRequest - 7, // 47: coder.tailnet.v2.Tailnet.Coordinate:input_type -> coder.tailnet.v2.CoordinateRequest - 13, // 48: coder.tailnet.v2.Tailnet.PostTelemetry:output_type -> coder.tailnet.v2.TelemetryResponse - 4, // 49: coder.tailnet.v2.Tailnet.StreamDERPMaps:output_type -> coder.tailnet.v2.DERPMap - 8, // 50: coder.tailnet.v2.Tailnet.Coordinate:output_type -> coder.tailnet.v2.CoordinateResponse - 48, // [48:51] is the sub-list for method output_type - 45, // [45:48] is the sub-list for method input_type - 45, // [45:45] is the sub-list for extension type_name - 45, // [45:45] is the sub-list for extension extendee - 0, // [0:45] is the sub-list for field type_name + 16, // 0: coder.tailnet.v2.DERPMap.home_params:type_name -> coder.tailnet.v2.DERPMap.HomeParams + 18, // 1: coder.tailnet.v2.DERPMap.regions:type_name -> coder.tailnet.v2.DERPMap.RegionsEntry + 32, // 2: coder.tailnet.v2.Node.as_of:type_name -> google.protobuf.Timestamp + 21, // 3: coder.tailnet.v2.Node.derp_latency:type_name -> coder.tailnet.v2.Node.DerpLatencyEntry + 22, // 4: coder.tailnet.v2.Node.derp_forced_websocket:type_name -> coder.tailnet.v2.Node.DerpForcedWebsocketEntry + 33, // 5: coder.tailnet.v2.RefreshResumeTokenResponse.refresh_in:type_name -> google.protobuf.Duration + 32, // 6: coder.tailnet.v2.RefreshResumeTokenResponse.expires_at:type_name -> google.protobuf.Timestamp + 23, // 7: coder.tailnet.v2.CoordinateRequest.update_self:type_name -> coder.tailnet.v2.CoordinateRequest.UpdateSelf + 24, // 8: coder.tailnet.v2.CoordinateRequest.disconnect:type_name -> coder.tailnet.v2.CoordinateRequest.Disconnect + 25, // 9: coder.tailnet.v2.CoordinateRequest.add_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel + 25, // 10: coder.tailnet.v2.CoordinateRequest.remove_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel + 26, // 11: coder.tailnet.v2.CoordinateRequest.ready_for_handshake:type_name -> coder.tailnet.v2.CoordinateRequest.ReadyForHandshake + 27, // 12: coder.tailnet.v2.CoordinateResponse.peer_updates:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate + 1, // 13: coder.tailnet.v2.IPFields.class:type_name -> coder.tailnet.v2.IPFields.IPClass + 34, // 14: coder.tailnet.v2.Netcheck.OSHasIPv6:type_name -> google.protobuf.BoolValue + 34, // 15: coder.tailnet.v2.Netcheck.MappingVariesByDestIP:type_name -> google.protobuf.BoolValue + 34, // 16: coder.tailnet.v2.Netcheck.HairPinning:type_name -> google.protobuf.BoolValue + 34, // 17: coder.tailnet.v2.Netcheck.UPnP:type_name -> google.protobuf.BoolValue + 34, // 18: coder.tailnet.v2.Netcheck.PMP:type_name -> google.protobuf.BoolValue + 34, // 19: coder.tailnet.v2.Netcheck.PCP:type_name -> google.protobuf.BoolValue + 28, // 20: coder.tailnet.v2.Netcheck.RegionV4Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV4LatencyEntry + 29, // 21: coder.tailnet.v2.Netcheck.RegionV6Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV6LatencyEntry + 30, // 22: coder.tailnet.v2.Netcheck.GlobalV4:type_name -> coder.tailnet.v2.Netcheck.NetcheckIP + 30, // 23: coder.tailnet.v2.Netcheck.GlobalV6:type_name -> coder.tailnet.v2.Netcheck.NetcheckIP + 32, // 24: coder.tailnet.v2.TelemetryEvent.time:type_name -> google.protobuf.Timestamp + 2, // 25: coder.tailnet.v2.TelemetryEvent.status:type_name -> coder.tailnet.v2.TelemetryEvent.Status + 3, // 26: coder.tailnet.v2.TelemetryEvent.client_type:type_name -> coder.tailnet.v2.TelemetryEvent.ClientType + 31, // 27: coder.tailnet.v2.TelemetryEvent.p2p_endpoint:type_name -> coder.tailnet.v2.TelemetryEvent.P2PEndpoint + 4, // 28: coder.tailnet.v2.TelemetryEvent.derp_map:type_name -> coder.tailnet.v2.DERPMap + 12, // 29: coder.tailnet.v2.TelemetryEvent.latest_netcheck:type_name -> coder.tailnet.v2.Netcheck + 33, // 30: coder.tailnet.v2.TelemetryEvent.connection_age:type_name -> google.protobuf.Duration + 33, // 31: coder.tailnet.v2.TelemetryEvent.connection_setup:type_name -> google.protobuf.Duration + 33, // 32: coder.tailnet.v2.TelemetryEvent.p2p_setup:type_name -> google.protobuf.Duration + 33, // 33: coder.tailnet.v2.TelemetryEvent.derp_latency:type_name -> google.protobuf.Duration + 33, // 34: coder.tailnet.v2.TelemetryEvent.p2p_latency:type_name -> google.protobuf.Duration + 35, // 35: coder.tailnet.v2.TelemetryEvent.throughput_mbits:type_name -> google.protobuf.FloatValue + 13, // 36: coder.tailnet.v2.TelemetryRequest.events:type_name -> coder.tailnet.v2.TelemetryEvent + 19, // 37: coder.tailnet.v2.DERPMap.HomeParams.region_score:type_name -> coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry + 20, // 38: coder.tailnet.v2.DERPMap.Region.nodes:type_name -> coder.tailnet.v2.DERPMap.Region.Node + 17, // 39: coder.tailnet.v2.DERPMap.RegionsEntry.value:type_name -> coder.tailnet.v2.DERPMap.Region + 6, // 40: coder.tailnet.v2.CoordinateRequest.UpdateSelf.node:type_name -> coder.tailnet.v2.Node + 6, // 41: coder.tailnet.v2.CoordinateResponse.PeerUpdate.node:type_name -> coder.tailnet.v2.Node + 0, // 42: coder.tailnet.v2.CoordinateResponse.PeerUpdate.kind:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind + 33, // 43: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry.value:type_name -> google.protobuf.Duration + 33, // 44: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry.value:type_name -> google.protobuf.Duration + 11, // 45: coder.tailnet.v2.Netcheck.NetcheckIP.fields:type_name -> coder.tailnet.v2.IPFields + 11, // 46: coder.tailnet.v2.TelemetryEvent.P2PEndpoint.fields:type_name -> coder.tailnet.v2.IPFields + 14, // 47: coder.tailnet.v2.Tailnet.PostTelemetry:input_type -> coder.tailnet.v2.TelemetryRequest + 5, // 48: coder.tailnet.v2.Tailnet.StreamDERPMaps:input_type -> coder.tailnet.v2.StreamDERPMapsRequest + 7, // 49: coder.tailnet.v2.Tailnet.RefreshResumeToken:input_type -> coder.tailnet.v2.RefreshResumeTokenRequest + 9, // 50: coder.tailnet.v2.Tailnet.Coordinate:input_type -> coder.tailnet.v2.CoordinateRequest + 15, // 51: coder.tailnet.v2.Tailnet.PostTelemetry:output_type -> coder.tailnet.v2.TelemetryResponse + 4, // 52: coder.tailnet.v2.Tailnet.StreamDERPMaps:output_type -> coder.tailnet.v2.DERPMap + 8, // 53: coder.tailnet.v2.Tailnet.RefreshResumeToken:output_type -> coder.tailnet.v2.RefreshResumeTokenResponse + 10, // 54: coder.tailnet.v2.Tailnet.Coordinate:output_type -> coder.tailnet.v2.CoordinateResponse + 51, // [51:55] is the sub-list for method output_type + 47, // [47:51] is the sub-list for method input_type + 47, // [47:47] is the sub-list for extension type_name + 47, // [47:47] is the sub-list for extension extendee + 0, // [0:47] is the sub-list for field type_name } func init() { file_tailnet_proto_tailnet_proto_init() } @@ -2244,7 +2371,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CoordinateRequest); i { + switch v := v.(*RefreshResumeTokenRequest); i { case 0: return &v.state case 1: @@ -2256,7 +2383,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CoordinateResponse); i { + switch v := v.(*RefreshResumeTokenResponse); i { case 0: return &v.state case 1: @@ -2268,7 +2395,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IPFields); i { + switch v := v.(*CoordinateRequest); i { case 0: return &v.state case 1: @@ -2280,7 +2407,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Netcheck); i { + switch v := v.(*CoordinateResponse); i { case 0: return &v.state case 1: @@ -2292,7 +2419,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TelemetryEvent); i { + switch v := v.(*IPFields); i { case 0: return &v.state case 1: @@ -2304,7 +2431,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TelemetryRequest); i { + switch v := v.(*Netcheck); i { case 0: return &v.state case 1: @@ -2316,7 +2443,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TelemetryResponse); i { + switch v := v.(*TelemetryEvent); i { case 0: return &v.state case 1: @@ -2328,7 +2455,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DERPMap_HomeParams); i { + switch v := v.(*TelemetryRequest); i { case 0: return &v.state case 1: @@ -2340,6 +2467,30 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TelemetryResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_tailnet_proto_tailnet_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DERPMap_HomeParams); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_tailnet_proto_tailnet_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DERPMap_Region); i { case 0: return &v.state @@ -2351,7 +2502,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DERPMap_Region_Node); i { case 0: return &v.state @@ -2363,7 +2514,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_UpdateSelf); i { case 0: return &v.state @@ -2375,7 +2526,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_Disconnect); i { case 0: return &v.state @@ -2387,7 +2538,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_Tunnel); i { case 0: return &v.state @@ -2399,7 +2550,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_ReadyForHandshake); i { case 0: return &v.state @@ -2411,7 +2562,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateResponse_PeerUpdate); i { case 0: return &v.state @@ -2423,7 +2574,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Netcheck_NetcheckIP); i { case 0: return &v.state @@ -2435,7 +2586,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TelemetryEvent_P2PEndpoint); i { case 0: return &v.state @@ -2454,7 +2605,7 @@ func file_tailnet_proto_tailnet_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_tailnet_proto_tailnet_proto_rawDesc, NumEnums: 4, - NumMessages: 26, + NumMessages: 28, NumExtensions: 0, NumServices: 1, }, diff --git a/tailnet/proto/tailnet.proto b/tailnet/proto/tailnet.proto index 30421fbf01852..f5ab91395f670 100644 --- a/tailnet/proto/tailnet.proto +++ b/tailnet/proto/tailnet.proto @@ -57,6 +57,14 @@ message Node { repeated string endpoints = 10; } +message RefreshResumeTokenRequest {} + +message RefreshResumeTokenResponse { + string token = 1; + google.protobuf.Duration refresh_in = 2; + google.protobuf.Timestamp expires_at = 3; +} + message CoordinateRequest { message UpdateSelf { Node node = 1; @@ -191,5 +199,6 @@ message TelemetryResponse {} service Tailnet { rpc PostTelemetry(TelemetryRequest) returns (TelemetryResponse); rpc StreamDERPMaps(StreamDERPMapsRequest) returns (stream DERPMap); + rpc RefreshResumeToken(RefreshResumeTokenRequest) returns (RefreshResumeTokenResponse); rpc Coordinate(stream CoordinateRequest) returns (stream CoordinateResponse); } diff --git a/tailnet/proto/tailnet_drpc.pb.go b/tailnet/proto/tailnet_drpc.pb.go index afb254c4726d4..c0c3fcef65249 100644 --- a/tailnet/proto/tailnet_drpc.pb.go +++ b/tailnet/proto/tailnet_drpc.pb.go @@ -40,6 +40,7 @@ type DRPCTailnetClient interface { PostTelemetry(ctx context.Context, in *TelemetryRequest) (*TelemetryResponse, error) StreamDERPMaps(ctx context.Context, in *StreamDERPMapsRequest) (DRPCTailnet_StreamDERPMapsClient, error) + RefreshResumeToken(ctx context.Context, in *RefreshResumeTokenRequest) (*RefreshResumeTokenResponse, error) Coordinate(ctx context.Context) (DRPCTailnet_CoordinateClient, error) } @@ -102,6 +103,15 @@ func (x *drpcTailnet_StreamDERPMapsClient) RecvMsg(m *DERPMap) error { return x.MsgRecv(m, drpcEncoding_File_tailnet_proto_tailnet_proto{}) } +func (c *drpcTailnetClient) RefreshResumeToken(ctx context.Context, in *RefreshResumeTokenRequest) (*RefreshResumeTokenResponse, error) { + out := new(RefreshResumeTokenResponse) + err := c.cc.Invoke(ctx, "/coder.tailnet.v2.Tailnet/RefreshResumeToken", drpcEncoding_File_tailnet_proto_tailnet_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + func (c *drpcTailnetClient) Coordinate(ctx context.Context) (DRPCTailnet_CoordinateClient, error) { stream, err := c.cc.NewStream(ctx, "/coder.tailnet.v2.Tailnet/Coordinate", drpcEncoding_File_tailnet_proto_tailnet_proto{}) if err != nil { @@ -144,6 +154,7 @@ func (x *drpcTailnet_CoordinateClient) RecvMsg(m *CoordinateResponse) error { type DRPCTailnetServer interface { PostTelemetry(context.Context, *TelemetryRequest) (*TelemetryResponse, error) StreamDERPMaps(*StreamDERPMapsRequest, DRPCTailnet_StreamDERPMapsStream) error + RefreshResumeToken(context.Context, *RefreshResumeTokenRequest) (*RefreshResumeTokenResponse, error) Coordinate(DRPCTailnet_CoordinateStream) error } @@ -157,13 +168,17 @@ func (s *DRPCTailnetUnimplementedServer) StreamDERPMaps(*StreamDERPMapsRequest, return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCTailnetUnimplementedServer) RefreshResumeToken(context.Context, *RefreshResumeTokenRequest) (*RefreshResumeTokenResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + func (s *DRPCTailnetUnimplementedServer) Coordinate(DRPCTailnet_CoordinateStream) error { return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } type DRPCTailnetDescription struct{} -func (DRPCTailnetDescription) NumMethods() int { return 3 } +func (DRPCTailnetDescription) NumMethods() int { return 4 } func (DRPCTailnetDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -186,6 +201,15 @@ func (DRPCTailnetDescription) Method(n int) (string, drpc.Encoding, drpc.Receive ) }, DRPCTailnetServer.StreamDERPMaps, true case 2: + return "/coder.tailnet.v2.Tailnet/RefreshResumeToken", drpcEncoding_File_tailnet_proto_tailnet_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCTailnetServer). + RefreshResumeToken( + ctx, + in1.(*RefreshResumeTokenRequest), + ) + }, DRPCTailnetServer.RefreshResumeToken, true + case 3: return "/coder.tailnet.v2.Tailnet/Coordinate", drpcEncoding_File_tailnet_proto_tailnet_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return nil, srv.(DRPCTailnetServer). @@ -231,6 +255,22 @@ func (x *drpcTailnet_StreamDERPMapsStream) Send(m *DERPMap) error { return x.MsgSend(m, drpcEncoding_File_tailnet_proto_tailnet_proto{}) } +type DRPCTailnet_RefreshResumeTokenStream interface { + drpc.Stream + SendAndClose(*RefreshResumeTokenResponse) error +} + +type drpcTailnet_RefreshResumeTokenStream struct { + drpc.Stream +} + +func (x *drpcTailnet_RefreshResumeTokenStream) SendAndClose(m *RefreshResumeTokenResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_tailnet_proto_tailnet_proto{}); err != nil { + return err + } + return x.CloseSend() +} + type DRPCTailnet_CoordinateStream interface { drpc.Stream Send(*CoordinateResponse) error diff --git a/tailnet/resume.go b/tailnet/resume.go new file mode 100644 index 0000000000000..98e3b02d1e35f --- /dev/null +++ b/tailnet/resume.go @@ -0,0 +1,117 @@ +package tailnet + +import ( + "encoding/json" + "time" + + "github.com/go-jose/go-jose/v3" + "github.com/google/uuid" + "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/coder/coder/v2/tailnet/proto" +) + +const ( + DefaultResumeTokenExpiry = 24 * time.Hour + + resumeTokenSigningAlgorithm = jose.HS512 +) + +var InsecureTestResumeTokenProvider ResumeTokenProvider = ResumeTokenKeyProvider{ + key: [64]byte{1}, + expiry: time.Hour, +} + +type ResumeTokenProvider interface { + GenerateResumeToken(peerID uuid.UUID) (*proto.RefreshResumeTokenResponse, error) + ParseResumeToken(token string) (uuid.UUID, error) +} + +type ResumeTokenKeyProvider struct { + key [64]byte + expiry time.Duration +} + +func NewResumeTokenKeyProvider(key [64]byte, expiry time.Duration) ResumeTokenProvider { + if expiry <= 0 { + expiry = DefaultResumeTokenExpiry + } + return ResumeTokenKeyProvider{ + key: key, + expiry: DefaultResumeTokenExpiry, + } +} + +type resumeTokenPayload struct { + PeerID uuid.UUID `json:"peer_id"` + Expiry time.Time `json:"expiry"` +} + +func (p ResumeTokenKeyProvider) GenerateResumeToken(peerID uuid.UUID) (*proto.RefreshResumeTokenResponse, error) { + payload := resumeTokenPayload{ + PeerID: peerID, + Expiry: time.Now().Add(p.expiry), + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, xerrors.Errorf("marshal payload to JSON: %w", err) + } + + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: resumeTokenSigningAlgorithm, + Key: p.key[:], + }, nil) + if err != nil { + return nil, xerrors.Errorf("create signer: %w", err) + } + + signedObject, err := signer.Sign(payloadBytes) + if err != nil { + return nil, xerrors.Errorf("sign payload: %w", err) + } + + serialized, err := signedObject.CompactSerialize() + if err != nil { + return nil, xerrors.Errorf("serialize JWS: %w", err) + } + + return &proto.RefreshResumeTokenResponse{ + Token: serialized, + RefreshIn: durationpb.New(p.expiry / 2), + ExpiresAt: timestamppb.New(payload.Expiry), + }, nil +} + +// VerifySignedToken parses a signed workspace app token with the given key and +// returns the payload. If the token is invalid or expired, an error is +// returned. +func (p ResumeTokenKeyProvider) ParseResumeToken(str string) (uuid.UUID, error) { + object, err := jose.ParseSigned(str) + if err != nil { + return uuid.Nil, xerrors.Errorf("parse JWS: %w", err) + } + if len(object.Signatures) != 1 { + return uuid.Nil, xerrors.New("expected 1 signature") + } + if object.Signatures[0].Header.Algorithm != string(resumeTokenSigningAlgorithm) { + return uuid.Nil, xerrors.Errorf("expected token signing algorithm to be %q, got %q", resumeTokenSigningAlgorithm, object.Signatures[0].Header.Algorithm) + } + + output, err := object.Verify(p.key[:]) + if err != nil { + return uuid.Nil, xerrors.Errorf("verify JWS: %w", err) + } + + var tok resumeTokenPayload + err = json.Unmarshal(output, &tok) + if err != nil { + return uuid.Nil, xerrors.Errorf("unmarshal payload: %w", err) + } + if tok.Expiry.Before(time.Now()) { + return uuid.Nil, xerrors.New("signed app token expired") + } + + return tok.PeerID, nil +} diff --git a/tailnet/service.go b/tailnet/service.go index 05af0cdc28d04..ebb5f7e9163a0 100644 --- a/tailnet/service.go +++ b/tailnet/service.go @@ -43,6 +43,7 @@ type ClientServiceOptions struct { DERPMapUpdateFrequency time.Duration DERPMapFn func() *tailcfg.DERPMap NetworkTelemetryHandler func(batch []*proto.TelemetryEvent) + ResumeTokenProvider ResumeTokenProvider } // ClientService is a tailnet coordination service that accepts a connection and version from a @@ -66,6 +67,7 @@ func NewClientService(options ClientServiceOptions) ( DerpMapUpdateFrequency: options.DERPMapUpdateFrequency, DerpMapFn: options.DERPMapFn, NetworkTelemetryHandler: options.NetworkTelemetryHandler, + ResumeTokenProvider: options.ResumeTokenProvider, } err := proto.DRPCRegisterTailnet(mux, drpcService) if err != nil { @@ -127,6 +129,7 @@ type DRPCService struct { DerpMapUpdateFrequency time.Duration DerpMapFn func() *tailcfg.DERPMap NetworkTelemetryHandler func(batch []*proto.TelemetryEvent) + ResumeTokenProvider ResumeTokenProvider } func (s *DRPCService) PostTelemetry(_ context.Context, req *proto.TelemetryRequest) (*proto.TelemetryResponse, error) { @@ -167,6 +170,19 @@ func (s *DRPCService) StreamDERPMaps(_ *proto.StreamDERPMapsRequest, stream prot } } +func (s *DRPCService) RefreshResumeToken(ctx context.Context, _ *proto.RefreshResumeTokenRequest) (*proto.RefreshResumeTokenResponse, error) { + streamID, ok := ctx.Value(streamIDContextKey{}).(StreamID) + if !ok { + return nil, xerrors.New("no Stream ID") + } + + res, err := s.ResumeTokenProvider.GenerateResumeToken(streamID.ID) + if err != nil { + return nil, xerrors.Errorf("generate resume token: %w", err) + } + return res, nil +} + func (s *DRPCService) Coordinate(stream proto.DRPCTailnet_CoordinateStream) error { ctx := stream.Context() streamID, ok := ctx.Value(streamIDContextKey{}).(StreamID) diff --git a/tailnet/service_test.go b/tailnet/service_test.go index 0b41d29eb1669..1940a1223a103 100644 --- a/tailnet/service_test.go +++ b/tailnet/service_test.go @@ -40,6 +40,7 @@ func TestClientService_ServeClient_V2(t *testing.T) { NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { telemetryEvents <- batch }, + ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, }) require.NoError(t, err) @@ -144,6 +145,7 @@ func TestClientService_ServeClient_V1(t *testing.T) { DERPMapUpdateFrequency: 0, DERPMapFn: nil, NetworkTelemetryHandler: nil, + ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, }) require.NoError(t, err) diff --git a/tailnet/test/integration/integration.go b/tailnet/test/integration/integration.go index 2f19ec43dec02..7c59eabed1990 100644 --- a/tailnet/test/integration/integration.go +++ b/tailnet/test/integration/integration.go @@ -181,6 +181,7 @@ func (o SimpleServerOptions) Router(t *testing.T, logger slog.Logger) *chi.Mux { } }, NetworkTelemetryHandler: func(batch []*tailnetproto.TelemetryEvent) {}, + ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, }) require.NoError(t, err) From 904fde2cce886b9ffd057d71bd3628b9c9393b7f Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 13 Aug 2024 05:03:05 +0000 Subject: [PATCH 2/6] PR comments --- cli/server.go | 30 +------ coderd/coderdtest/coderdtest.go | 2 +- coderd/workspaceagents.go | 7 +- codersdk/workspacesdk/connector.go | 20 ++--- .../workspacesdk/connector_internal_test.go | 22 +++-- .../wsproxy/wsproxysdk/wsproxysdk_test.go | 2 +- tailnet/coordinator_test.go | 4 +- tailnet/resume.go | 81 ++++++++++++++++--- tailnet/service_test.go | 4 +- tailnet/test/integration/integration.go | 2 +- 10 files changed, 111 insertions(+), 63 deletions(-) diff --git a/cli/server.go b/cli/server.go index 33db837f2640b..9218f63f5726b 100644 --- a/cli/server.go +++ b/cli/server.go @@ -805,35 +805,9 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // Read the coordinator resume token signing key from the // database. - resumeTokenKey := [64]byte{} - resumeTokenKeyStr, err := tx.GetCoordinatorResumeTokenSigningKey(ctx) - if err != nil && !xerrors.Is(err, sql.ErrNoRows) { - return xerrors.Errorf("get coordinator resume token key: %w", err) - } - if decoded, err := hex.DecodeString(resumeTokenKeyStr); err != nil || len(decoded) != len(resumeTokenKey) { - b := make([]byte, len(resumeTokenKey)) - _, err := rand.Read(b) - if err != nil { - return xerrors.Errorf("generate fresh coordinator resume token key: %w", err) - } - - resumeTokenKeyStr = hex.EncodeToString(b) - err = tx.UpsertCoordinatorResumeTokenSigningKey(ctx, resumeTokenKeyStr) - if err != nil { - return xerrors.Errorf("insert freshly generated coordinator resume token key to database: %w", err) - } - } - - resumeTokenKeyBytes, err := hex.DecodeString(resumeTokenKeyStr) + resumeTokenKey, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, tx) if err != nil { - return xerrors.Errorf("decode coordinator resume token key from database: %w", err) - } - if len(resumeTokenKeyBytes) != len(resumeTokenKey) { - return xerrors.Errorf("coordinator resume token key in database is not the correct length, expect %d got %d", len(resumeTokenKey), len(resumeTokenKeyBytes)) - } - copy(resumeTokenKey[:], resumeTokenKeyBytes) - if resumeTokenKey == [64]byte{} { - return xerrors.Errorf("coordinator resume token key in database is empty") + return xerrors.Errorf("get coordinator resume token key from database: %w", err) } options.CoordinatorResumeTokenProvider = tailnet.NewResumeTokenKeyProvider(resumeTokenKey, tailnet.DefaultResumeTokenExpiry) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index ceeeb75465c89..68eb48b5f5eb9 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -492,7 +492,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can TailnetCoordinator: options.Coordinator, BaseDERPMap: derpMap, DERPMapUpdateFrequency: 150 * time.Millisecond, - CoordinatorResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, + CoordinatorResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval, AgentStatsRefreshInterval: options.AgentStatsRefreshInterval, DeploymentValues: options.DeploymentValues, diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 3bfd057ffb181..75f2a06045af7 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -853,11 +853,14 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R ) if resumeToken != "" { var err error - peerID, err = api.Options.CoordinatorResumeTokenProvider.ParseResumeToken(resumeToken) + peerID, err = api.Options.CoordinatorResumeTokenProvider.VerifyResumeToken(resumeToken) if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{ Message: workspacesdk.CoordinateAPIInvalidResumeToken, Detail: err.Error(), + Validations: []codersdk.ValidationError{ + {Field: "resume_token", Detail: workspacesdk.CoordinateAPIInvalidResumeToken}, + }, }) return } diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index e897036a4bfdb..c16c56d1444ca 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -178,20 +178,22 @@ func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { close(tac.connected) } if err != nil { - if !errors.Is(err, context.Canceled) { - tac.logger.Error(tac.ctx, "failed to dial tailnet v2+ API", slog.Error(err)) - } - if res.StatusCode == http.StatusBadRequest { - err = codersdk.ReadBodyAsError(res) - var sdkErr *codersdk.Error - if xerrors.As(err, &sdkErr) { - if sdkErr.Message == CoordinateAPIInvalidResumeToken { + didLog := false + bodyErr := codersdk.ReadBodyAsError(res) + var sdkErr *codersdk.Error + if xerrors.As(bodyErr, &sdkErr) { + for _, v := range sdkErr.Validations { + if v.Field == "resume_token" { // Unset the resume token for the next attempt - tac.logger.Debug(tac.ctx, "server replied invalid resume token; unsetting for next connection attempt") + tac.logger.Warn(tac.ctx, "failed to dial tailnet v2+ API: server replied invalid resume token; unsetting for next connection attempt") tac.resumeToken.Store(nil) + didLog = true } } } + if !didLog && !errors.Is(err, context.Canceled) { + tac.logger.Error(tac.ctx, "failed to dial tailnet v2+ API", slog.Error(err), slog.F("sdk_err", sdkErr)) + } return nil, err } client, err := tailnet.NewDRPCClient( diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index 3b200c20b10cb..0d4ea88925f3b 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -61,7 +61,7 @@ func TestTailnetAPIConnector_Disconnects(t *testing.T) { DERPMapUpdateFrequency: time.Millisecond, DERPMapFn: func() *tailcfg.DERPMap { return <-derpMapCh }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) {}, - ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), }) require.NoError(t, err) @@ -185,12 +185,15 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { t.Logf("received resume token: %s", resumeToken) assert.Equal(t, expectResumeToken, resumeToken) if resumeToken != "" { - peerID, err = resumeTokenProvider.ParseResumeToken(resumeToken) + peerID, err = resumeTokenProvider.VerifyResumeToken(resumeToken) assert.NoError(t, err, "failed to parse resume token") if err != nil { - httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ + httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{ Message: CoordinateAPIInvalidResumeToken, Detail: err.Error(), + Validations: []codersdk.ValidationError{ + {Field: "resume_token", Detail: CoordinateAPIInvalidResumeToken}, + }, }) return } @@ -253,8 +256,8 @@ func (r resumeTokenProvider) GenerateResumeToken(peerID uuid.UUID) (*proto.Refre return r.genFn(peerID) } -// ParseResumeToken implements tailnet.ResumeTokenProvider. -func (r resumeTokenProvider) ParseResumeToken(token string) (uuid.UUID, error) { +// VerifyResumeToken implements tailnet.ResumeTokenProvider. +func (r resumeTokenProvider) VerifyResumeToken(token string) (uuid.UUID, error) { return r.parseFn(token) } @@ -307,12 +310,15 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { ) t.Logf("received resume token: %s", resumeToken) if resumeToken != "" { - _, err = resumeTokenProvider.ParseResumeToken(resumeToken) + _, err = resumeTokenProvider.VerifyResumeToken(resumeToken) assert.Error(t, err, "parse resume token should return an error") atomic.AddInt64(&didFail, 1) - httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ + httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{ Message: CoordinateAPIInvalidResumeToken, Detail: err.Error(), + Validations: []codersdk.ValidationError{ + {Field: "resume_token", Detail: CoordinateAPIInvalidResumeToken}, + }, }) return } @@ -385,7 +391,7 @@ func TestTailnetAPIConnector_TelemetrySuccess(t *testing.T) { NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { testutil.RequireSendCtx(ctx, t, eventCh, batch) }, - ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), }) require.NoError(t, err) diff --git a/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go b/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go index 456f046d7a148..70bcd25290eb1 100644 --- a/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go +++ b/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go @@ -177,7 +177,7 @@ func TestDialCoordinator(t *testing.T) { DERPMapUpdateFrequency: time.Hour, DERPMapFn: func() *tailcfg.DERPMap { panic("not implemented") }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { panic("not implemented") }, - ResumeTokenProvider: agpl.InsecureTestResumeTokenProvider, + ResumeTokenProvider: agpl.NewInsecureTestResumeTokenProvider(), }) require.NoError(t, err) diff --git a/tailnet/coordinator_test.go b/tailnet/coordinator_test.go index 218fc9d3def2b..196c9e5fcee18 100644 --- a/tailnet/coordinator_test.go +++ b/tailnet/coordinator_test.go @@ -630,7 +630,7 @@ func TestRemoteCoordination(t *testing.T) { DERPMapUpdateFrequency: time.Hour, DERPMapFn: func() *tailcfg.DERPMap { panic("not implemented") }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { panic("not implemented") }, - ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), }) require.NoError(t, err) sC, cC := net.Pipe() @@ -682,7 +682,7 @@ func TestRemoteCoordination_SendsReadyForHandshake(t *testing.T) { DERPMapUpdateFrequency: time.Hour, DERPMapFn: func() *tailcfg.DERPMap { panic("not implemented") }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { panic("not implemented") }, - ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), }) require.NoError(t, err) sC, cC := net.Pipe() diff --git a/tailnet/resume.go b/tailnet/resume.go index 98e3b02d1e35f..f4f3604d33847 100644 --- a/tailnet/resume.go +++ b/tailnet/resume.go @@ -1,6 +1,10 @@ package tailnet import ( + "context" + "crypto/rand" + "database/sql" + "encoding/hex" "encoding/json" "time" @@ -19,22 +23,81 @@ const ( resumeTokenSigningAlgorithm = jose.HS512 ) -var InsecureTestResumeTokenProvider ResumeTokenProvider = ResumeTokenKeyProvider{ - key: [64]byte{1}, - expiry: time.Hour, +// NewInsecureTestResumeTokenProvider returns a ResumeTokenProvider that uses a +// random key with short expiry for testing purposes. If any errors occur while +// generating the key, the function panics. +func NewInsecureTestResumeTokenProvider() ResumeTokenProvider { + key, err := GenerateResumeTokenSigningKey() + if err != nil { + panic(err) + } + return NewResumeTokenKeyProvider(key, time.Hour) } type ResumeTokenProvider interface { GenerateResumeToken(peerID uuid.UUID) (*proto.RefreshResumeTokenResponse, error) - ParseResumeToken(token string) (uuid.UUID, error) + VerifyResumeToken(token string) (uuid.UUID, error) +} + +type ResumeTokenSigningKey [64]byte + +func GenerateResumeTokenSigningKey() (ResumeTokenSigningKey, error) { + var key ResumeTokenSigningKey + _, err := rand.Read(key[:]) + if err != nil { + return key, xerrors.Errorf("generate random key: %w", err) + } + return key, nil +} + +type ResumeTokenSigningKeyDatabaseStore interface { + GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) + UpsertCoordinatorResumeTokenSigningKey(ctx context.Context, key string) error +} + +// ResumeTokenSigningKeyFromDatabase retrieves the coordinator resume token +// signing key from the database. If the key is not found, a new key is +// generated and inserted into the database. +func ResumeTokenSigningKeyFromDatabase(ctx context.Context, db ResumeTokenSigningKeyDatabaseStore) (ResumeTokenSigningKey, error) { + var resumeTokenKey ResumeTokenSigningKey + resumeTokenKeyStr, err := db.GetCoordinatorResumeTokenSigningKey(ctx) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + return resumeTokenKey, xerrors.Errorf("get coordinator resume token key: %w", err) + } + if decoded, err := hex.DecodeString(resumeTokenKeyStr); err != nil || len(decoded) != len(resumeTokenKey) { + b := make([]byte, len(resumeTokenKey)) + _, err := rand.Read(b) + if err != nil { + return resumeTokenKey, xerrors.Errorf("generate fresh coordinator resume token key: %w", err) + } + + resumeTokenKeyStr = hex.EncodeToString(b) + err = db.UpsertCoordinatorResumeTokenSigningKey(ctx, resumeTokenKeyStr) + if err != nil { + return resumeTokenKey, xerrors.Errorf("insert freshly generated coordinator resume token key to database: %w", err) + } + } + + resumeTokenKeyBytes, err := hex.DecodeString(resumeTokenKeyStr) + if err != nil { + return resumeTokenKey, xerrors.Errorf("decode coordinator resume token key from database: %w", err) + } + if len(resumeTokenKeyBytes) != len(resumeTokenKey) { + return resumeTokenKey, xerrors.Errorf("coordinator resume token key in database is not the correct length, expect %d got %d", len(resumeTokenKey), len(resumeTokenKeyBytes)) + } + copy(resumeTokenKey[:], resumeTokenKeyBytes) + if resumeTokenKey == [64]byte{} { + return resumeTokenKey, xerrors.Errorf("coordinator resume token key in database is empty") + } + return resumeTokenKey, nil } type ResumeTokenKeyProvider struct { - key [64]byte + key ResumeTokenSigningKey expiry time.Duration } -func NewResumeTokenKeyProvider(key [64]byte, expiry time.Duration) ResumeTokenProvider { +func NewResumeTokenKeyProvider(key ResumeTokenSigningKey, expiry time.Duration) ResumeTokenProvider { if expiry <= 0 { expiry = DefaultResumeTokenExpiry } @@ -45,8 +108,8 @@ func NewResumeTokenKeyProvider(key [64]byte, expiry time.Duration) ResumeTokenPr } type resumeTokenPayload struct { - PeerID uuid.UUID `json:"peer_id"` - Expiry time.Time `json:"expiry"` + PeerID uuid.UUID `json:"sub"` + Expiry time.Time `json:"exp"` } func (p ResumeTokenKeyProvider) GenerateResumeToken(peerID uuid.UUID) (*proto.RefreshResumeTokenResponse, error) { @@ -87,7 +150,7 @@ func (p ResumeTokenKeyProvider) GenerateResumeToken(peerID uuid.UUID) (*proto.Re // VerifySignedToken parses a signed workspace app token with the given key and // returns the payload. If the token is invalid or expired, an error is // returned. -func (p ResumeTokenKeyProvider) ParseResumeToken(str string) (uuid.UUID, error) { +func (p ResumeTokenKeyProvider) VerifyResumeToken(str string) (uuid.UUID, error) { object, err := jose.ParseSigned(str) if err != nil { return uuid.Nil, xerrors.Errorf("parse JWS: %w", err) diff --git a/tailnet/service_test.go b/tailnet/service_test.go index 1940a1223a103..71a7fdacd8dda 100644 --- a/tailnet/service_test.go +++ b/tailnet/service_test.go @@ -40,7 +40,7 @@ func TestClientService_ServeClient_V2(t *testing.T) { NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { telemetryEvents <- batch }, - ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), }) require.NoError(t, err) @@ -145,7 +145,7 @@ func TestClientService_ServeClient_V1(t *testing.T) { DERPMapUpdateFrequency: 0, DERPMapFn: nil, NetworkTelemetryHandler: nil, - ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), }) require.NoError(t, err) diff --git a/tailnet/test/integration/integration.go b/tailnet/test/integration/integration.go index 7c59eabed1990..41326caaa7e4e 100644 --- a/tailnet/test/integration/integration.go +++ b/tailnet/test/integration/integration.go @@ -181,7 +181,7 @@ func (o SimpleServerOptions) Router(t *testing.T, logger slog.Logger) *chi.Mux { } }, NetworkTelemetryHandler: func(batch []*tailnetproto.TelemetryEvent) {}, - ResumeTokenProvider: tailnet.InsecureTestResumeTokenProvider, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), }) require.NoError(t, err) From 73a5cee7decf517aed130e001f550f57864ddff0 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 14 Aug 2024 09:54:48 +0000 Subject: [PATCH 3/6] PR comments 2 --- cli/server.go | 3 +- coderd/workspaceagents_test.go | 21 +- .../workspacesdk/connector_internal_test.go | 56 +----- tailnet/resume.go | 18 +- tailnet/resume_test.go | 181 ++++++++++++++++++ testutil/ctx.go | 13 ++ 6 files changed, 221 insertions(+), 71 deletions(-) create mode 100644 tailnet/resume_test.go diff --git a/cli/server.go b/cli/server.go index 9218f63f5726b..80d0449fdc85d 100644 --- a/cli/server.go +++ b/cli/server.go @@ -56,6 +56,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/pretty" + "github.com/coder/quartz" "github.com/coder/retry" "github.com/coder/serpent" "github.com/coder/wgtunnel/tunnelsdk" @@ -809,7 +810,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. if err != nil { return xerrors.Errorf("get coordinator resume token key from database: %w", err) } - options.CoordinatorResumeTokenProvider = tailnet.NewResumeTokenKeyProvider(resumeTokenKey, tailnet.DefaultResumeTokenExpiry) + options.CoordinatorResumeTokenProvider = tailnet.NewResumeTokenKeyProvider(resumeTokenKey, quartz.NewReal(), tailnet.DefaultResumeTokenExpiry) return nil }, nil) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 80e40b36f0a56..2e2fa1f6bb268 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -517,7 +517,7 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - // We block DERP in this test to ensure that even if there's no direct + // We block direct in this test to ensure that even if there's no direct // connection, no shenanigans happen with the peer IDs on either side. dv := coderdtest.DeploymentValues(t) err := dv.DERP.Config.BlockDirect.Set("true") @@ -563,22 +563,17 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { proxyClient := codersdk.New(proxyURL) proxyClient.SetSessionToken(client.SessionToken()) - // Connect from a client. - conn, err := func() (*workspacesdk.AgentConn, error) { - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() // Connection should remain open even if the dial context is canceled. + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() - return workspacesdk.New(proxyClient). - DialAgent(ctx, agentID, &workspacesdk.DialAgentOptions{ - Logger: logger.Named("client"), - }) - }() + // Connect from a client. + conn, err := workspacesdk.New(proxyClient). + DialAgent(ctx, agentID, &workspacesdk.DialAgentOptions{ + Logger: logger.Named("client"), + }) require.NoError(t, err) defer conn.Close() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - ok := conn.AwaitReachable(ctx) require.True(t, ok) originalAgentPeers := agentCloser.TailnetConn().GetKnownPeerIDs() diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index 0d4ea88925f3b..3adddbba6dbd3 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -30,6 +30,7 @@ import ( "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/coder/v2/tailnet/tailnettest" "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" ) func init() { @@ -159,7 +160,7 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { derpMapCh := make(chan *tailcfg.DERPMap) defer close(derpMapCh) - resumeTokenProvider := tailnet.NewResumeTokenKeyProvider([64]byte{1}, time.Second) + resumeTokenProvider := tailnet.NewResumeTokenKeyProvider([64]byte{1}, quartz.NewReal(), time.Second) svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ Logger: logger, CoordPtr: &coordPtr, @@ -198,7 +199,7 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { return } } - testutil.RequireSendCtx(ctx, t, peerIDCh, peerID) + testutil.AssertSendCtx(ctx, t, peerIDCh, peerID) sws, err := websocket.Accept(w, r, nil) if !assert.NoError(t, err) { @@ -244,23 +245,6 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { require.Equal(t, originalPeerID, testutil.RequireRecvCtx(ctx, t, peerIDCh)) } -type resumeTokenProvider struct { - genFn func(uuid.UUID) (*proto.RefreshResumeTokenResponse, error) - parseFn func(string) (uuid.UUID, error) -} - -var _ tailnet.ResumeTokenProvider = resumeTokenProvider{} - -// GenerateResumeToken implements tailnet.ResumeTokenProvider. -func (r resumeTokenProvider) GenerateResumeToken(peerID uuid.UUID) (*proto.RefreshResumeTokenResponse, error) { - return r.genFn(peerID) -} - -// VerifyResumeToken implements tailnet.ResumeTokenProvider. -func (r resumeTokenProvider) VerifyResumeToken(token string) (uuid.UUID, error) { - return r.parseFn(token) -} - func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) @@ -275,43 +259,22 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { derpMapCh := make(chan *tailcfg.DERPMap) defer close(derpMapCh) - resumeTokenProvider := resumeTokenProvider{ - genFn: func(uuid.UUID) (*proto.RefreshResumeTokenResponse, error) { - return &proto.RefreshResumeTokenResponse{ - Token: uuid.NewString(), - RefreshIn: durationpb.New(time.Minute), - ExpiresAt: timestamppb.New(time.Now().Add(time.Hour)), - }, nil - }, - parseFn: func(string) (uuid.UUID, error) { - return uuid.UUID{}, xerrors.New("test error") - }, - } svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ Logger: logger, CoordPtr: &coordPtr, DERPMapUpdateFrequency: time.Millisecond, DERPMapFn: func() *tailcfg.DERPMap { return <-derpMapCh }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) {}, - ResumeTokenProvider: resumeTokenProvider, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), }) require.NoError(t, err) var ( websocketConnCh = make(chan *websocket.Conn, 64) - peerIDCh = make(chan uuid.UUID, 64) didFail int64 ) svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Accept a resume_token query parameter to use the same peer ID. - var ( - peerID = uuid.New() - resumeToken = r.URL.Query().Get("resume_token") - ) - t.Logf("received resume token: %s", resumeToken) - if resumeToken != "" { - _, err = resumeTokenProvider.VerifyResumeToken(resumeToken) - assert.Error(t, err, "parse resume token should return an error") + if r.URL.Query().Get("resume_token") != "" { atomic.AddInt64(&didFail, 1) httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{ Message: CoordinateAPIInvalidResumeToken, @@ -322,7 +285,6 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { }) return } - testutil.RequireSendCtx(ctx, t, peerIDCh, peerID) sws, err := websocket.Accept(w, r, nil) if !assert.NoError(t, err) { @@ -332,7 +294,7 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { ctx, nc := codersdk.WebsocketNetConn(r.Context(), sws, websocket.MessageBinary) err = svc.ServeConnV2(ctx, nc, tailnet.StreamID{ Name: "client", - ID: peerID, + ID: uuid.New(), Auth: tailnet.ClientCoordinateeAuth{AgentID: agentID}, }) assert.NoError(t, err) @@ -353,7 +315,6 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { // Sever the connection and expect it to reconnect with the resume token, // which should fail and cause the client to be disconnected. The client // should then reconnect with no resume token. - originalPeerID := testutil.RequireRecvCtx(ctx, t, peerIDCh) wsConn := testutil.RequireRecvCtx(ctx, t, websocketConnCh) _ = wsConn.Close(websocket.StatusGoingAway, "test") @@ -363,10 +324,7 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { return rt != nil && rt.Token != originalResumeToken.Token }, testutil.WaitShort, testutil.IntervalFast) - // Peer ID should be different. - require.NotEqual(t, originalPeerID, testutil.RequireRecvCtx(ctx, t, peerIDCh)) - - // The resume token should have failed to parse. + // The resume token should have been rejected by the server. require.EqualValues(t, 1, atomic.LoadInt64(&didFail)) } diff --git a/tailnet/resume.go b/tailnet/resume.go index f4f3604d33847..443dfd3eb8b6f 100644 --- a/tailnet/resume.go +++ b/tailnet/resume.go @@ -15,6 +15,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/coder/coder/v2/tailnet/proto" + "github.com/coder/quartz" ) const ( @@ -31,7 +32,7 @@ func NewInsecureTestResumeTokenProvider() ResumeTokenProvider { if err != nil { panic(err) } - return NewResumeTokenKeyProvider(key, time.Hour) + return NewResumeTokenKeyProvider(key, quartz.NewReal(), time.Hour) } type ResumeTokenProvider interface { @@ -65,13 +66,12 @@ func ResumeTokenSigningKeyFromDatabase(ctx context.Context, db ResumeTokenSignin return resumeTokenKey, xerrors.Errorf("get coordinator resume token key: %w", err) } if decoded, err := hex.DecodeString(resumeTokenKeyStr); err != nil || len(decoded) != len(resumeTokenKey) { - b := make([]byte, len(resumeTokenKey)) - _, err := rand.Read(b) + newKey, err := GenerateResumeTokenSigningKey() if err != nil { return resumeTokenKey, xerrors.Errorf("generate fresh coordinator resume token key: %w", err) } - resumeTokenKeyStr = hex.EncodeToString(b) + resumeTokenKeyStr = hex.EncodeToString(newKey[:]) err = db.UpsertCoordinatorResumeTokenSigningKey(ctx, resumeTokenKeyStr) if err != nil { return resumeTokenKey, xerrors.Errorf("insert freshly generated coordinator resume token key to database: %w", err) @@ -94,15 +94,17 @@ func ResumeTokenSigningKeyFromDatabase(ctx context.Context, db ResumeTokenSignin type ResumeTokenKeyProvider struct { key ResumeTokenSigningKey + clock quartz.Clock expiry time.Duration } -func NewResumeTokenKeyProvider(key ResumeTokenSigningKey, expiry time.Duration) ResumeTokenProvider { +func NewResumeTokenKeyProvider(key ResumeTokenSigningKey, clock quartz.Clock, expiry time.Duration) ResumeTokenProvider { if expiry <= 0 { expiry = DefaultResumeTokenExpiry } return ResumeTokenKeyProvider{ key: key, + clock: clock, expiry: DefaultResumeTokenExpiry, } } @@ -115,7 +117,7 @@ type resumeTokenPayload struct { func (p ResumeTokenKeyProvider) GenerateResumeToken(peerID uuid.UUID) (*proto.RefreshResumeTokenResponse, error) { payload := resumeTokenPayload{ PeerID: peerID, - Expiry: time.Now().Add(p.expiry), + Expiry: p.clock.Now().Add(p.expiry), } payloadBytes, err := json.Marshal(payload) if err != nil { @@ -172,8 +174,8 @@ func (p ResumeTokenKeyProvider) VerifyResumeToken(str string) (uuid.UUID, error) if err != nil { return uuid.Nil, xerrors.Errorf("unmarshal payload: %w", err) } - if tok.Expiry.Before(time.Now()) { - return uuid.Nil, xerrors.New("signed app token expired") + if tok.Expiry.Before(p.clock.Now()) { + return uuid.Nil, xerrors.New("signed resume token expired") } return tok.PeerID, nil diff --git a/tailnet/resume_test.go b/tailnet/resume_test.go new file mode 100644 index 0000000000000..c88d4e3710ab2 --- /dev/null +++ b/tailnet/resume_test.go @@ -0,0 +1,181 @@ +package tailnet_test + +import ( + "encoding/hex" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/tailnet" + "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" +) + +func TestResumeTokenSigningKeyFromDatabase(t *testing.T) { + t.Parallel() + + assertRandomKey := func(t *testing.T, key tailnet.ResumeTokenSigningKey) { + t.Helper() + assert.NotEqual(t, tailnet.ResumeTokenSigningKey{}, key, "key is empty") + assert.NotEqualValues(t, [64]byte{1}, key, "key is all 1s") + } + + t.Run("GenerateRetrieve", func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitShort) + key1, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) + require.NoError(t, err) + assertRandomKey(t, key1) + + key2, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) + require.NoError(t, err) + require.Equal(t, key1, key2, "keys are different") + }) + + t.Run("GetError", func(t *testing.T) { + t.Parallel() + + db := dbmock.NewMockStore(gomock.NewController(t)) + db.EXPECT().GetCoordinatorResumeTokenSigningKey(gomock.Any()).Return("", assert.AnError) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) + require.Error(t, err) + require.ErrorIs(t, err, assert.AnError) + }) + + t.Run("UpsertError", func(t *testing.T) { + t.Parallel() + + db := dbmock.NewMockStore(gomock.NewController(t)) + db.EXPECT().GetCoordinatorResumeTokenSigningKey(gomock.Any()).Return("", nil) + db.EXPECT().UpsertCoordinatorResumeTokenSigningKey(gomock.Any(), gomock.Any()).Return(assert.AnError) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) + require.Error(t, err) + require.ErrorIs(t, err, assert.AnError) + }) + + t.Run("DecodeErrorShouldRegenerate", func(t *testing.T) { + t.Parallel() + + db := dbmock.NewMockStore(gomock.NewController(t)) + db.EXPECT().GetCoordinatorResumeTokenSigningKey(gomock.Any()).Return("invalid", nil) + db.EXPECT().UpsertCoordinatorResumeTokenSigningKey(gomock.Any(), gomock.Any()).Return(nil) + + ctx := testutil.Context(t, testutil.WaitShort) + key, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) + require.NoError(t, err) + assertRandomKey(t, key) + }) + + t.Run("LengthErrorShouldRegenerate", func(t *testing.T) { + t.Parallel() + + db := dbmock.NewMockStore(gomock.NewController(t)) + db.EXPECT().GetCoordinatorResumeTokenSigningKey(gomock.Any()).Return("deadbeef", nil) + db.EXPECT().UpsertCoordinatorResumeTokenSigningKey(gomock.Any(), gomock.Any()).Return(nil) + + ctx := testutil.Context(t, testutil.WaitShort) + key, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) + require.NoError(t, err) + assertRandomKey(t, key) + }) + + t.Run("EmptyError", func(t *testing.T) { + t.Parallel() + + db := dbmock.NewMockStore(gomock.NewController(t)) + emptyKey := hex.EncodeToString(make([]byte, 64)) + db.EXPECT().GetCoordinatorResumeTokenSigningKey(gomock.Any()).Return(emptyKey, nil) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) + require.Error(t, err) + require.ErrorContains(t, err, "is empty") + }) +} + +func TestResumeTokenKeyProvider(t *testing.T) { + t.Parallel() + + key, err := tailnet.GenerateResumeTokenSigningKey() + require.NoError(t, err) + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + id := uuid.New() + now := time.Now() + clock := quartz.NewMock(t) + clock.Set(now) + provider := tailnet.NewResumeTokenKeyProvider(key, clock, tailnet.DefaultResumeTokenExpiry) + token, err := provider.GenerateResumeToken(id) + require.NoError(t, err) + require.NotNil(t, token) + require.NotEmpty(t, token.Token) + require.Equal(t, tailnet.DefaultResumeTokenExpiry/2, token.RefreshIn.AsDuration()) + require.WithinDuration(t, now.Add(tailnet.DefaultResumeTokenExpiry), token.ExpiresAt.AsTime(), time.Second) + + gotID, err := provider.VerifyResumeToken(token.Token) + require.NoError(t, err) + require.Equal(t, id, gotID) + }) + + t.Run("Expired", func(t *testing.T) { + t.Parallel() + + id := uuid.New() + now := time.Now() + clock := quartz.NewMock(t) + _ = clock.Set(now) + provider := tailnet.NewResumeTokenKeyProvider(key, clock, tailnet.DefaultResumeTokenExpiry) + token, err := provider.GenerateResumeToken(id) + require.NoError(t, err) + require.NotNil(t, token) + require.NotEmpty(t, token.Token) + require.Equal(t, tailnet.DefaultResumeTokenExpiry/2, token.RefreshIn.AsDuration()) + require.WithinDuration(t, now.Add(tailnet.DefaultResumeTokenExpiry), token.ExpiresAt.AsTime(), time.Second) + + // Advance time past expiry + _ = clock.Advance(tailnet.DefaultResumeTokenExpiry + time.Second) + + _, err = provider.VerifyResumeToken(token.Token) + require.Error(t, err) + require.ErrorContains(t, err, "expired") + }) + + t.Run("InvalidToken", func(t *testing.T) { + t.Parallel() + + provider := tailnet.NewResumeTokenKeyProvider(key, quartz.NewMock(t), tailnet.DefaultResumeTokenExpiry) + _, err := provider.VerifyResumeToken("invalid") + require.Error(t, err) + require.ErrorContains(t, err, "parse JWS") + }) + + t.Run("VerifyError", func(t *testing.T) { + t.Parallel() + + // Generate a resume token with a different key + otherKey, err := tailnet.GenerateResumeTokenSigningKey() + require.NoError(t, err) + otherProvider := tailnet.NewResumeTokenKeyProvider(otherKey, quartz.NewMock(t), tailnet.DefaultResumeTokenExpiry) + token, err := otherProvider.GenerateResumeToken(uuid.New()) + require.NoError(t, err) + + provider := tailnet.NewResumeTokenKeyProvider(key, quartz.NewMock(t), tailnet.DefaultResumeTokenExpiry) + _, err = provider.VerifyResumeToken(token.Token) + require.Error(t, err) + require.ErrorContains(t, err, "verify JWS") + }) +} diff --git a/testutil/ctx.go b/testutil/ctx.go index c8f8c1769fe7f..b1179dfdf554a 100644 --- a/testutil/ctx.go +++ b/testutil/ctx.go @@ -23,6 +23,9 @@ func RequireRecvCtx[A any](ctx context.Context, t testing.TB, c <-chan A) (a A) } } +// NOTE: no AssertRecvCtx because it'd be bad if we returned a default value on +// the cases it times out. + func RequireSendCtx[A any](ctx context.Context, t testing.TB, c chan<- A, a A) { t.Helper() select { @@ -32,3 +35,13 @@ func RequireSendCtx[A any](ctx context.Context, t testing.TB, c chan<- A, a A) { // OK! } } + +func AssertSendCtx[A any](ctx context.Context, t testing.TB, c chan<- A, a A) { + t.Helper() + select { + case <-ctx.Done(): + t.Error("timeout") + case c <- a: + // OK! + } +} From 3db51e070fb062c83fcd30e1676bd5e0240b0e75 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sun, 18 Aug 2024 08:25:19 +0000 Subject: [PATCH 4/6] PR comments 3 --- coderd/workspaceagents_test.go | 232 +++++++++--------- codersdk/workspacesdk/connector.go | 31 ++- .../workspacesdk/connector_internal_test.go | 103 +++++--- codersdk/workspacesdk/workspacesdk.go | 3 +- enterprise/tailnet/workspaceproxy.go | 9 +- tailnet/resume.go | 14 +- 6 files changed, 215 insertions(+), 177 deletions(-) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 2e2fa1f6bb268..a171297c1b3ce 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -6,12 +6,9 @@ import ( "fmt" "net" "net/http" - "net/http/httptest" - "net/url" "runtime" "strconv" "strings" - "sync" "sync/atomic" "testing" "time" @@ -21,6 +18,7 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/xerrors" "google.golang.org/protobuf/types/known/timestamppb" + "nhooyr.io/websocket" "tailscale.com/tailcfg" "cdr.dev/slog" @@ -43,6 +41,8 @@ import ( "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisionersdk/proto" + "github.com/coder/coder/v2/tailnet" + tailnetproto "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/coder/v2/tailnet/tailnettest" "github.com/coder/coder/v2/testutil" ) @@ -512,111 +512,138 @@ func TestWorkspaceAgentClientCoordinate_BadVersion(t *testing.T) { require.Equal(t, "version", sdkErr.Validations[0].Field) } +type resumeTokenTestFakeCoordinator struct { + tailnet.Coordinator + lastPeerID uuid.UUID +} + +var _ tailnet.Coordinator = &resumeTokenTestFakeCoordinator{} + +func (c *resumeTokenTestFakeCoordinator) ServeClient(conn net.Conn, id uuid.UUID, agentID uuid.UUID) error { + c.lastPeerID = id + return c.Coordinator.ServeClient(conn, id, agentID) +} + +func (c *resumeTokenTestFakeCoordinator) Coordinate(ctx context.Context, id uuid.UUID, name string, a tailnet.CoordinateeAuth) (chan<- *tailnetproto.CoordinateRequest, <-chan *tailnetproto.CoordinateResponse) { + c.lastPeerID = id + return c.Coordinator.Coordinate(ctx, id, name, a) +} + func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { t.Parallel() logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - - // We block direct in this test to ensure that even if there's no direct - // connection, no shenanigans happen with the peer IDs on either side. - dv := coderdtest.DeploymentValues(t) - err := dv.DERP.Config.BlockDirect.Set("true") - require.NoError(t, err) + coordinator := &resumeTokenTestFakeCoordinator{ + Coordinator: tailnet.NewCoordinator(logger), + } client, closer, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ - DeploymentValues: dv, + Coordinator: coordinator, }) defer closer.Close() user := coderdtest.CreateFirstUser(t, client) - // Change the DERP mapper to our custom one. - var currentDerpMap atomic.Pointer[tailcfg.DERPMap] - originalDerpMap, _ := tailnettest.RunDERPAndSTUN(t) - currentDerpMap.Store(originalDerpMap) - derpMapFn := func(_ *tailcfg.DERPMap) *tailcfg.DERPMap { - return currentDerpMap.Load().Clone() - } - api.DERPMapper.Store(&derpMapFn) - - // Start workspace a workspace agent. + // Create a workspace with an agent. No need to connect it since clients can + // still connect to the coordinator while the agent isn't connected. r := dbfake.WorkspaceBuild(t, api.Database, database.Workspace{ OrganizationID: user.OrganizationID, OwnerID: user.UserID, }).WithAgent().Do() - - agentCloser := agenttest.New(t, client.URL, r.AgentToken) - resources := coderdtest.AwaitWorkspaceAgents(t, client, r.Workspace.ID) - agentID := resources[0].Agents[0].ID - - // Create a new "proxy" server that we can use to kill the connection - // whenever we want. - l, err := netListenDroppable("tcp", "localhost:0") + agentTokenUUID, err := uuid.Parse(r.AgentToken) require.NoError(t, err) - defer l.Close() - srv := &httptest.Server{ - Listener: l, - //nolint:gosec - Config: &http.Server{Handler: api.RootHandler}, - } - srv.Start() - proxyURL, err := url.Parse(srv.URL) + ctx := testutil.Context(t, testutil.WaitLong) + agentAndBuild, err := api.Database.GetWorkspaceAgentAndLatestBuildByAuthToken(dbauthz.AsSystemRestricted(ctx), agentTokenUUID) //nolint require.NoError(t, err) - proxyClient := codersdk.New(proxyURL) - proxyClient.SetSessionToken(client.SessionToken()) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - // Connect from a client. - conn, err := workspacesdk.New(proxyClient). - DialAgent(ctx, agentID, &workspacesdk.DialAgentOptions{ - Logger: logger.Named("client"), - }) + // Connect with no resume token, and ensure that the peer ID is set to a + // random value. + coordinator.lastPeerID = uuid.Nil + originalResumeToken, err := connectToCoordinatorAndFetchResumeToken(ctx, logger, client, agentAndBuild.WorkspaceAgent.ID, "") require.NoError(t, err) - defer conn.Close() + originalPeerID := coordinator.lastPeerID + require.NotEqual(t, originalPeerID, uuid.Nil) - ok := conn.AwaitReachable(ctx) - require.True(t, ok) - originalAgentPeers := agentCloser.TailnetConn().GetKnownPeerIDs() + // Connect with a valid resume token, and ensure that the peer ID is set to + // the stored value. + coordinator.lastPeerID = uuid.Nil + newResumeToken, err := connectToCoordinatorAndFetchResumeToken(ctx, logger, client, agentAndBuild.WorkspaceAgent.ID, originalResumeToken) + require.NoError(t, err) + require.Equal(t, originalPeerID, coordinator.lastPeerID) + require.NotEqual(t, originalResumeToken, newResumeToken) - // Drop client conn's coordinator connection. - l.DropAllConns() + // Connect with an invalid resume token, and ensure that the request is + // rejected. + coordinator.lastPeerID = uuid.Nil + _, err = connectToCoordinatorAndFetchResumeToken(ctx, logger, client, agentAndBuild.WorkspaceAgent.ID, "invalid") + require.Error(t, err) + var sdkErr *codersdk.Error + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode()) + require.Len(t, sdkErr.Validations, 1) + require.Equal(t, "resume_token", sdkErr.Validations[0].Field) + require.Equal(t, uuid.Nil, coordinator.lastPeerID) +} - // HACK: Change the DERP map and add a second "marker" region so we know - // when the client has reconnected to the coordinator. - // - // With some refactoring of the client connection to expose the - // coordinator connection status, this wouldn't be needed, but this - // also works. - derpMap := currentDerpMap.Load().Clone() - newDerpMap, _ := tailnettest.RunDERPAndSTUN(t) - derpMap.Regions[2] = newDerpMap.Regions[1] - currentDerpMap.Store(derpMap) +// connectToCoordinatorAndFetchResumeToken connects to the tailnet coordinator +// with a given resume token. It returns an error if the connection is rejected. +// If the connection is accepted, it is immediately closed and no error is +// returned. +func connectToCoordinatorAndFetchResumeToken(ctx context.Context, logger slog.Logger, sdkClient *codersdk.Client, agentID uuid.UUID, resumeToken string) (string, error) { + u, err := sdkClient.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/coordinate", agentID)) + if err != nil { + return "", xerrors.Errorf("parse URL: %w", err) + } + q := u.Query() + q.Set("version", "2.0") + if resumeToken != "" { + q.Set("resume_token", resumeToken) + } + u.RawQuery = q.Encode() - // Wait for the agent's DERP map to be updated. - require.Eventually(t, func() bool { - conn := agentCloser.TailnetConn() - if conn == nil { - return false + //nolint:bodyclose + wsConn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ + HTTPHeader: http.Header{ + "Coder-Session-Token": []string{sdkClient.SessionToken()}, + }, + }) + if err != nil { + if resp.StatusCode != http.StatusSwitchingProtocols { + err = codersdk.ReadBodyAsError(resp) } - regionIDs := conn.DERPMap().RegionIDs() - return len(regionIDs) == 2 && regionIDs[1] == 2 - }, testutil.WaitLong, testutil.IntervalFast) - - // Wait for the DERP map to be updated on the client. This means that the - // client has reconnected to the coordinator. - require.Eventually(t, func() bool { - regionIDs := conn.Conn.DERPMap().RegionIDs() - return len(regionIDs) == 2 && regionIDs[1] == 2 - }, testutil.WaitLong, testutil.IntervalFast) + return "", xerrors.Errorf("websocket dial: %w", err) + } + defer wsConn.Close(websocket.StatusNormalClosure, "done") + + // Send a request to the server to ensure that we're plumbed all the way + // through. + rpcClient, err := tailnet.NewDRPCClient( + websocket.NetConn(ctx, wsConn, websocket.MessageBinary), + logger, + ) + if err != nil { + return "", xerrors.Errorf("new dRPC client: %w", err) + } - // The first client should still be able to reach the agent. - ok = conn.AwaitReachable(ctx) - require.True(t, ok) - _, err = conn.ListeningPorts(ctx) - require.NoError(t, err) + // Send an empty coordination request. This will do nothing on the server, + // but ensures our wrapped coordinator can record the peer ID. + coordinateClient, err := rpcClient.Coordinate(ctx) + if err != nil { + return "", xerrors.Errorf("coordinate: %w", err) + } + err = coordinateClient.Send(&tailnetproto.CoordinateRequest{}) + if err != nil { + return "", xerrors.Errorf("send empty coordination request: %w", err) + } + err = coordinateClient.Close() + if err != nil { + return "", xerrors.Errorf("close coordination request: %w", err) + } - // The agent should not see any new peers. - require.ElementsMatch(t, originalAgentPeers, agentCloser.TailnetConn().GetKnownPeerIDs()) + // Fetch a resume token. + newResumeToken, err := rpcClient.RefreshResumeToken(ctx, &tailnetproto.RefreshResumeTokenRequest{}) + if err != nil { + return "", xerrors.Errorf("fetch resume token: %w", err) + } + return newResumeToken.Token, nil } func TestWorkspaceAgentTailnetDirectDisabled(t *testing.T) { @@ -1832,40 +1859,3 @@ func postStartup(ctx context.Context, t testing.TB, client agent.Client, startup _, err = aAPI.UpdateStartup(ctx, &agentproto.UpdateStartupRequest{Startup: startup}) return err } - -type droppableTCPListener struct { - net.Listener - mu sync.Mutex - conns []net.Conn -} - -var _ net.Listener = &droppableTCPListener{} - -func netListenDroppable(network, addr string) (*droppableTCPListener, error) { - l, err := net.Listen(network, addr) - if err != nil { - return nil, err - } - return &droppableTCPListener{Listener: l}, nil -} - -func (l *droppableTCPListener) Accept() (net.Conn, error) { - conn, err := l.Listener.Accept() - if err != nil { - return nil, err - } - - l.mu.Lock() - defer l.mu.Unlock() - l.conns = append(l.conns, conn) - return conn, nil -} - -func (l *droppableTCPListener) DropAllConns() { - l.mu.Lock() - defer l.mu.Unlock() - for _, c := range l.conns { - _ = c.Close() - } - l.conns = nil -} diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index c16c56d1444ca..1b804dee08321 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -25,6 +25,7 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" + "github.com/coder/quartz" "github.com/coder/retry" ) @@ -62,6 +63,7 @@ type tailnetAPIConnector struct { agentID uuid.UUID coordinateURL string + clock quartz.Clock dialOptions *websocket.DialOptions conn tailnetConn customDialFn func() (proto.DRPCTailnetClient, error) @@ -70,7 +72,7 @@ type tailnetAPIConnector struct { client proto.DRPCTailnetClient connected chan error - resumeToken atomic.Pointer[proto.RefreshResumeTokenResponse] + resumeToken *proto.RefreshResumeTokenResponse isFirst bool closed chan struct{} @@ -80,12 +82,13 @@ type tailnetAPIConnector struct { } // Create a new tailnetAPIConnector without running it -func newTailnetAPIConnector(ctx context.Context, logger slog.Logger, agentID uuid.UUID, coordinateURL string, dialOptions *websocket.DialOptions) *tailnetAPIConnector { +func newTailnetAPIConnector(ctx context.Context, logger slog.Logger, agentID uuid.UUID, coordinateURL string, clock quartz.Clock, dialOptions *websocket.DialOptions) *tailnetAPIConnector { return &tailnetAPIConnector{ ctx: ctx, logger: logger, agentID: agentID, coordinateURL: coordinateURL, + clock: clock, dialOptions: dialOptions, conn: nil, connected: make(chan error, 1), @@ -98,7 +101,7 @@ func newTailnetAPIConnector(ctx context.Context, logger slog.Logger, agentID uui func (tac *tailnetAPIConnector) manageGracefulTimeout() { defer tac.cancelGracefulCtx() <-tac.ctx.Done() - timer := time.NewTimer(tailnetConnectorGracefulTimeout) + timer := tac.clock.NewTimer(tailnetConnectorGracefulTimeout, "tailnetAPIClient", "gracefulTimeout") defer timer.Stop() select { case <-tac.closed: @@ -114,6 +117,8 @@ func (tac *tailnetAPIConnector) runConnector(conn tailnetConn) { go func() { tac.isFirst = true defer close(tac.closed) + // Sadly retry doesn't support quartz.Clock yet so this is not + // influenced by the configured clock. for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); { tailnetClient, err := tac.dial() if err != nil { @@ -145,12 +150,11 @@ func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { if err != nil { return nil, xerrors.Errorf("parse URL %q: %w", tac.coordinateURL, err) } - resumeToken := tac.resumeToken.Load() - if resumeToken != nil { + if tac.resumeToken != nil { q := u.Query() - q.Set("resume_token", resumeToken.Token) + q.Set("resume_token", tac.resumeToken.Token) u.RawQuery = q.Encode() - tac.logger.Debug(tac.ctx, "using resume token", slog.F("resume_token", resumeToken)) + tac.logger.Debug(tac.ctx, "using resume token", slog.F("resume_token", tac.resumeToken)) } coordinateURL := u.String() @@ -186,7 +190,7 @@ func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { if v.Field == "resume_token" { // Unset the resume token for the next attempt tac.logger.Warn(tac.ctx, "failed to dial tailnet v2+ API: server replied invalid resume token; unsetting for next connection attempt") - tac.resumeToken.Store(nil) + tac.resumeToken = nil didLog = true } } @@ -317,7 +321,7 @@ func (tac *tailnetAPIConnector) derpMap(client proto.DRPCTailnetClient) error { } func (tac *tailnetAPIConnector) refreshToken(ctx context.Context, client proto.DRPCTailnetClient) { - ticker := time.NewTicker(15 * time.Second) + ticker := tac.clock.NewTicker(15*time.Second, "tailnetAPIConnector", "refreshToken") defer ticker.Stop() initialCh := make(chan struct{}, 1) @@ -341,8 +345,13 @@ func (tac *tailnetAPIConnector) refreshToken(ctx context.Context, client proto.D return } tac.logger.Debug(tac.ctx, "refreshed coordinator resume token", slog.F("resume_token", res)) - tac.resumeToken.Store(res) - ticker.Reset(res.RefreshIn.AsDuration()) + tac.resumeToken = res + dur := res.RefreshIn.AsDuration() + if dur <= 0 { + // A sensible delay to refresh again. + dur = 30 * time.Minute + } + ticker.Reset(dur, "tailnetAPIConnector", "refreshToken", "reset") } } diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index 79fcdfcb9c6b6..01bc9c2c1355d 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -82,7 +82,7 @@ func TestTailnetAPIConnector_Disconnects(t *testing.T) { fConn := newFakeTailnetConn() - uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}) + uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, quartz.NewReal(), &websocket.DialOptions{}) uut.runConnector(fConn) call := testutil.RequireRecvCtx(ctx, t, fCoord.CoordinateCalls) @@ -135,7 +135,7 @@ func TestTailnetAPIConnector_UplevelVersion(t *testing.T) { fConn := newFakeTailnetConn() - uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}) + uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, quartz.NewReal(), &websocket.DialOptions{}) uut.runConnector(fConn) err := testutil.RequireRecvCtx(ctx, t, uut.connected) @@ -160,7 +160,7 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { derpMapCh := make(chan *tailcfg.DERPMap) defer close(derpMapCh) - resumeTokenProvider := tailnet.NewResumeTokenKeyProvider([64]byte{1}, quartz.NewReal(), time.Second) + resumeTokenProvider := tailnet.NewInsecureTestResumeTokenProvider() svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ Logger: logger, CoordPtr: &coordPtr, @@ -173,7 +173,6 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { var ( websocketConnCh = make(chan *websocket.Conn, 64) - peerIDCh = make(chan uuid.UUID, 64) expectResumeToken = "" ) svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -199,7 +198,6 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { return } } - testutil.AssertSendCtx(ctx, t, peerIDCh, peerID) sws, err := websocket.Accept(w, r, nil) if !assert.NoError(t, err) { @@ -217,32 +215,50 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { fConn := newFakeTailnetConn() - uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}) + clock := quartz.NewMock(t) + newTickerTrap := clock.Trap().NewTicker("tailnetAPIConnector", "refreshToken") + tickerResetTrap := clock.Trap().TickerReset("tailnetAPIConnector", "refreshToken", "reset") + defer newTickerTrap.Close() + uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, clock, &websocket.DialOptions{}) uut.runConnector(fConn) - // Wait for the resume token to be fetched for the first time. - require.Eventually(t, func() bool { - return uut.resumeToken.Load() != nil - }, testutil.WaitShort, testutil.IntervalFast) - originalResumeToken := uut.resumeToken.Load() - require.NotNil(t, originalResumeToken) - expectResumeToken = originalResumeToken.Token + // Fetch first token. + trappedTicker := newTickerTrap.MustWait(ctx) + trappedTicker.Release() + waiter := clock.Advance(trappedTicker.Duration) + waiter.MustWait(ctx) + // We call ticker.Reset after each token fetch to apply the refresh duration + // requested by the server. + trappedReset := tickerResetTrap.MustWait(ctx) + trappedReset.Release() + require.NotNil(t, uut.resumeToken) + originalResumeToken := uut.resumeToken.Token + + // Fetch second token. + waiter = clock.Advance(trappedReset.Duration) + waiter.MustWait(ctx) + trappedReset = tickerResetTrap.MustWait(ctx) + trappedReset.Release() + require.NotNil(t, uut.resumeToken) + require.NotEqual(t, originalResumeToken, uut.resumeToken.Token) + expectResumeToken = uut.resumeToken.Token t.Logf("expecting resume token: %s", expectResumeToken) - // Sever the connection and expect it to reconnect with the resume token and - // assume the same peer ID. - originalPeerID := testutil.RequireRecvCtx(ctx, t, peerIDCh) + // Sever the connection and expect it to reconnect with the resume token. wsConn := testutil.RequireRecvCtx(ctx, t, websocketConnCh) _ = wsConn.Close(websocket.StatusGoingAway, "test") // Wait for the resume token to be refreshed. - require.Eventually(t, func() bool { - rt := uut.resumeToken.Load() - return rt != nil && rt.Token != originalResumeToken.Token - }, testutil.WaitShort, testutil.IntervalFast) - - // Peer ID should be identical. - require.Equal(t, originalPeerID, testutil.RequireRecvCtx(ctx, t, peerIDCh)) + trappedTicker = newTickerTrap.MustWait(ctx) + trappedTicker.Release() + waiter = clock.Advance(trappedTicker.Duration) + waiter.MustWait(ctx) + trappedReset = tickerResetTrap.MustWait(ctx) + trappedReset.Release() + + // The resume token should have changed again. + require.NotNil(t, uut.resumeToken) + require.NotEqual(t, expectResumeToken, uut.resumeToken.Token) } func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { @@ -301,15 +317,21 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { fConn := newFakeTailnetConn() - uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}) + clock := quartz.NewMock(t) + newTickerTrap := clock.Trap().NewTicker("tailnetAPIConnector", "refreshToken") + tickerResetTrap := clock.Trap().TickerReset("tailnetAPIConnector", "refreshToken", "reset") + defer newTickerTrap.Close() + uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, clock, &websocket.DialOptions{}) uut.runConnector(fConn) // Wait for the resume token to be fetched for the first time. - require.Eventually(t, func() bool { - return uut.resumeToken.Load() != nil - }, testutil.WaitShort, testutil.IntervalFast) - originalResumeToken := uut.resumeToken.Load() - require.NotNil(t, originalResumeToken) + trappedTicker := newTickerTrap.MustWait(ctx) + trappedTicker.Release() + waiter := clock.Advance(trappedTicker.Duration) + waiter.MustWait(ctx) + trappedReset := tickerResetTrap.MustWait(ctx) + trappedReset.Release() + originalResumeToken := uut.resumeToken.Token // Sever the connection and expect it to reconnect with the resume token, // which should fail and cause the client to be disconnected. The client @@ -317,11 +339,20 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { wsConn := testutil.RequireRecvCtx(ctx, t, websocketConnCh) _ = wsConn.Close(websocket.StatusGoingAway, "test") - // Wait for the resume token to be refreshed. - require.Eventually(t, func() bool { - rt := uut.resumeToken.Load() - return rt != nil && rt.Token != originalResumeToken.Token - }, testutil.WaitShort, testutil.IntervalFast) + // Wait for the resume token to be refreshed, which indicates a successful + // reconnect. + trappedTicker = newTickerTrap.MustWait(ctx) + trappedTicker.Release() + // Since we failed the initial reconnect and we're definitely reconnected + // now, the stored resume token should now be nil. + require.Nil(t, uut.resumeToken) + // Continue to the next token fetch. + waiter = clock.Advance(trappedTicker.Duration) + waiter.MustWait(ctx) + trappedReset = tickerResetTrap.MustWait(ctx) + trappedReset.Release() + require.NotNil(t, uut.resumeToken) + require.NotEqual(t, originalResumeToken, uut.resumeToken.Token) // The resume token should have been rejected by the server. require.EqualValues(t, 1, atomic.LoadInt64(&didFail)) @@ -368,7 +399,7 @@ func TestTailnetAPIConnector_TelemetrySuccess(t *testing.T) { fConn := newFakeTailnetConn() - uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}) + uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, quartz.NewReal(), &websocket.DialOptions{}) uut.runConnector(fConn) require.Eventually(t, func() bool { uut.clientMu.Lock() @@ -399,6 +430,7 @@ func TestTailnetAPIConnector_TelemetryUnimplemented(t *testing.T) { logger: logger, agentID: agentID, coordinateURL: "", + clock: quartz.NewReal(), dialOptions: &websocket.DialOptions{}, conn: nil, connected: make(chan error, 1), @@ -439,6 +471,7 @@ func TestTailnetAPIConnector_TelemetryNotRecognised(t *testing.T) { logger: logger, agentID: agentID, coordinateURL: "", + clock: quartz.NewReal(), dialOptions: &websocket.DialOptions{}, conn: nil, connected: make(chan error, 1), diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index b0797c0cc789a..e4b64f5702eba 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -22,6 +22,7 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" + "github.com/coder/quartz" ) // AgentIP is a static IPv6 address with the Tailscale prefix that is used to route @@ -236,7 +237,7 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options * q.Add("version", "2.0") coordinateURL.RawQuery = q.Encode() - connector := newTailnetAPIConnector(ctx, options.Logger, agentID, coordinateURL.String(), + connector := newTailnetAPIConnector(ctx, options.Logger, agentID, coordinateURL.String(), quartz.NewReal(), &websocket.DialOptions{ HTTPClient: c.client.HTTPClient, HTTPHeader: headers, diff --git a/enterprise/tailnet/workspaceproxy.go b/enterprise/tailnet/workspaceproxy.go index c786be72a99f6..dcadc4805de60 100644 --- a/enterprise/tailnet/workspaceproxy.go +++ b/enterprise/tailnet/workspaceproxy.go @@ -24,14 +24,7 @@ type ClientService struct { // NewClientService returns a ClientService based on the given Coordinator pointer. The pointer is // loaded on each processed connection. func NewClientService(options agpl.ClientServiceOptions) (*ClientService, error) { - s, err := agpl.NewClientService(agpl.ClientServiceOptions{ - Logger: options.Logger, - CoordPtr: options.CoordPtr, - DERPMapUpdateFrequency: options.DERPMapUpdateFrequency, - DERPMapFn: options.DERPMapFn, - NetworkTelemetryHandler: options.NetworkTelemetryHandler, - ResumeTokenProvider: options.ResumeTokenProvider, - }) + s, err := agpl.NewClientService(options) if err != nil { return nil, err } diff --git a/tailnet/resume.go b/tailnet/resume.go index 443dfd3eb8b6f..9f7f9601df182 100644 --- a/tailnet/resume.go +++ b/tailnet/resume.go @@ -24,6 +24,11 @@ const ( resumeTokenSigningAlgorithm = jose.HS512 ) +// resumeTokenSigningKeyID is a fixed key ID for the resume token signing key. +// If/when we add support for multiple keys (e.g. key rotation), this will move +// to the database instead. +var resumeTokenSigningKeyID = uuid.MustParse("97166747-9309-4d7f-9071-a230e257c2a4") + // NewInsecureTestResumeTokenProvider returns a ResumeTokenProvider that uses a // random key with short expiry for testing purposes. If any errors occur while // generating the key, the function panics. @@ -127,7 +132,11 @@ func (p ResumeTokenKeyProvider) GenerateResumeToken(peerID uuid.UUID) (*proto.Re signer, err := jose.NewSigner(jose.SigningKey{ Algorithm: resumeTokenSigningAlgorithm, Key: p.key[:], - }, nil) + }, &jose.SignerOptions{ + ExtraHeaders: map[jose.HeaderKey]interface{}{ + "kid": resumeTokenSigningKeyID.String(), + }, + }) if err != nil { return nil, xerrors.Errorf("create signer: %w", err) } @@ -163,6 +172,9 @@ func (p ResumeTokenKeyProvider) VerifyResumeToken(str string) (uuid.UUID, error) if object.Signatures[0].Header.Algorithm != string(resumeTokenSigningAlgorithm) { return uuid.Nil, xerrors.Errorf("expected token signing algorithm to be %q, got %q", resumeTokenSigningAlgorithm, object.Signatures[0].Header.Algorithm) } + if object.Signatures[0].Header.KeyID != resumeTokenSigningKeyID.String() { + return uuid.Nil, xerrors.Errorf("expected token key ID to be %q, got %q", resumeTokenSigningKeyID, object.Signatures[0].Header.KeyID) + } output, err := object.Verify(p.key[:]) if err != nil { From a81eac1e38d5890cef46c851623f9e16fb599ee9 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 20 Aug 2024 03:14:37 +0000 Subject: [PATCH 5/6] PR comments 4 --- coderd/coderdtest/coderdtest.go | 44 ++++++++++--------- coderd/database/dbauthz/dbauthz.go | 2 +- coderd/database/dbauthz/dbauthz_test.go | 2 +- coderd/workspaceagents_test.go | 10 ++++- codersdk/workspacesdk/connector.go | 5 +-- .../workspacesdk/connector_internal_test.go | 25 +++++------ tailnet/resume.go | 12 ++--- tailnet/resume_test.go | 28 ++++++------ 8 files changed, 71 insertions(+), 57 deletions(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 68eb48b5f5eb9..afa2130212217 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -96,25 +96,26 @@ type Options struct { // AccessURL denotes a custom access URL. By default we use the httptest // server's URL. Setting this may result in unexpected behavior (especially // with running agents). - AccessURL *url.URL - AppHostname string - AWSCertificates awsidentity.Certificates - Authorizer rbac.Authorizer - AzureCertificates x509.VerifyOptions - GithubOAuth2Config *coderd.GithubOAuth2Config - RealIPConfig *httpmw.RealIPConfig - OIDCConfig *coderd.OIDCConfig - GoogleTokenValidator *idtoken.Validator - SSHKeygenAlgorithm gitsshkey.Algorithm - AutobuildTicker <-chan time.Time - AutobuildStats chan<- autobuild.Stats - Auditor audit.Auditor - TLSCertificates []tls.Certificate - ExternalAuthConfigs []*externalauth.Config - TrialGenerator func(ctx context.Context, body codersdk.LicensorTrialRequest) error - RefreshEntitlements func(ctx context.Context) error - TemplateScheduleStore schedule.TemplateScheduleStore - Coordinator tailnet.Coordinator + AccessURL *url.URL + AppHostname string + AWSCertificates awsidentity.Certificates + Authorizer rbac.Authorizer + AzureCertificates x509.VerifyOptions + GithubOAuth2Config *coderd.GithubOAuth2Config + RealIPConfig *httpmw.RealIPConfig + OIDCConfig *coderd.OIDCConfig + GoogleTokenValidator *idtoken.Validator + SSHKeygenAlgorithm gitsshkey.Algorithm + AutobuildTicker <-chan time.Time + AutobuildStats chan<- autobuild.Stats + Auditor audit.Auditor + TLSCertificates []tls.Certificate + ExternalAuthConfigs []*externalauth.Config + TrialGenerator func(ctx context.Context, body codersdk.LicensorTrialRequest) error + RefreshEntitlements func(ctx context.Context) error + TemplateScheduleStore schedule.TemplateScheduleStore + Coordinator tailnet.Coordinator + CoordinatorResumeTokenProvider tailnet.ResumeTokenProvider HealthcheckFunc func(ctx context.Context, apiKey string) *healthsdk.HealthcheckReport HealthcheckTimeout time.Duration @@ -240,6 +241,9 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can if options.Database == nil { options.Database, options.Pubsub = dbtestutil.NewDB(t) } + if options.CoordinatorResumeTokenProvider == nil { + options.CoordinatorResumeTokenProvider = tailnet.NewInsecureTestResumeTokenProvider() + } if options.NotificationsEnqueuer == nil { options.NotificationsEnqueuer = new(testutil.FakeNotificationsEnqueuer) @@ -492,7 +496,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can TailnetCoordinator: options.Coordinator, BaseDERPMap: derpMap, DERPMapUpdateFrequency: 150 * time.Millisecond, - CoordinatorResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), + CoordinatorResumeTokenProvider: options.CoordinatorResumeTokenProvider, MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval, AgentStatsRefreshInterval: options.AgentStatsRefreshInterval, DeploymentValues: options.DeploymentValues, diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index cb4345440cb0c..283453e41520e 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1367,7 +1367,7 @@ func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUI } func (q *querier) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return "", err } return q.db.GetCoordinatorResumeTokenSigningKey(ctx) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index f0bb461e38786..4330f7ed6c579 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2567,7 +2567,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { })) s.Run("GetCoordinatorResumeTokenSigningKey", s.Subtest(func(db database.Store, check *expects) { db.UpsertCoordinatorResumeTokenSigningKey(context.Background(), "foo") - check.Args().Asserts(rbac.ResourceSystem, policy.ActionUpdate) + check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("InsertMissingGroups", s.Subtest(func(db database.Store, check *expects) { check.Args(database.InsertMissingGroupsParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate).Errors(errMatchAny) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index a171297c1b3ce..e43cc47bed693 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -45,6 +45,7 @@ import ( tailnetproto "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/coder/v2/tailnet/tailnettest" "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" ) func TestWorkspaceAgent(t *testing.T) { @@ -533,11 +534,16 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { t.Parallel() logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + clock := quartz.NewMock(t) + resumeTokenSigningKey, err := tailnet.GenerateResumeTokenSigningKey() + require.NoError(t, err) + resumeTokenProvider := tailnet.NewResumeTokenKeyProvider(resumeTokenSigningKey, clock, time.Hour) coordinator := &resumeTokenTestFakeCoordinator{ Coordinator: tailnet.NewCoordinator(logger), } client, closer, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ - Coordinator: coordinator, + Coordinator: coordinator, + CoordinatorResumeTokenProvider: resumeTokenProvider, }) defer closer.Close() user := coderdtest.CreateFirstUser(t, client) @@ -564,6 +570,7 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { // Connect with a valid resume token, and ensure that the peer ID is set to // the stored value. + clock.Advance(time.Second) coordinator.lastPeerID = uuid.Nil newResumeToken, err := connectToCoordinatorAndFetchResumeToken(ctx, logger, client, agentAndBuild.WorkspaceAgent.ID, originalResumeToken) require.NoError(t, err) @@ -572,6 +579,7 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { // Connect with an invalid resume token, and ensure that the request is // rejected. + clock.Advance(time.Second) coordinator.lastPeerID = uuid.Nil _, err = connectToCoordinatorAndFetchResumeToken(ctx, logger, client, agentAndBuild.WorkspaceAgent.ID, "invalid") require.Error(t, err) diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index 1b804dee08321..c761c92ae3e51 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -182,7 +182,6 @@ func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { close(tac.connected) } if err != nil { - didLog := false bodyErr := codersdk.ReadBodyAsError(res) var sdkErr *codersdk.Error if xerrors.As(bodyErr, &sdkErr) { @@ -191,11 +190,11 @@ func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { // Unset the resume token for the next attempt tac.logger.Warn(tac.ctx, "failed to dial tailnet v2+ API: server replied invalid resume token; unsetting for next connection attempt") tac.resumeToken = nil - didLog = true + return nil, err } } } - if !didLog && !errors.Is(err, context.Canceled) { + if !errors.Is(err, context.Canceled) { tac.logger.Error(tac.ctx, "failed to dial tailnet v2+ API", slog.Error(err), slog.F("sdk_err", sdkErr)) } return nil, err diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index 01bc9c2c1355d..c517042f1622f 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -160,7 +160,10 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { derpMapCh := make(chan *tailcfg.DERPMap) defer close(derpMapCh) - resumeTokenProvider := tailnet.NewInsecureTestResumeTokenProvider() + clock := quartz.NewMock(t) + resumeTokenSigningKey, err := tailnet.GenerateResumeTokenSigningKey() + require.NoError(t, err) + resumeTokenProvider := tailnet.NewResumeTokenKeyProvider(resumeTokenSigningKey, clock, time.Hour) svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ Logger: logger, CoordPtr: &coordPtr, @@ -215,18 +218,16 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { fConn := newFakeTailnetConn() - clock := quartz.NewMock(t) newTickerTrap := clock.Trap().NewTicker("tailnetAPIConnector", "refreshToken") tickerResetTrap := clock.Trap().TickerReset("tailnetAPIConnector", "refreshToken", "reset") defer newTickerTrap.Close() uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, clock, &websocket.DialOptions{}) uut.runConnector(fConn) - // Fetch first token. + // Fetch first token. We don't need to advance the clock since we use a + // channel with a single item to immediately fetch. trappedTicker := newTickerTrap.MustWait(ctx) trappedTicker.Release() - waiter := clock.Advance(trappedTicker.Duration) - waiter.MustWait(ctx) // We call ticker.Reset after each token fetch to apply the refresh duration // requested by the server. trappedReset := tickerResetTrap.MustWait(ctx) @@ -235,7 +236,7 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { originalResumeToken := uut.resumeToken.Token // Fetch second token. - waiter = clock.Advance(trappedReset.Duration) + waiter := clock.Advance(trappedReset.Duration) waiter.MustWait(ctx) trappedReset = tickerResetTrap.MustWait(ctx) trappedReset.Release() @@ -275,13 +276,17 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { derpMapCh := make(chan *tailcfg.DERPMap) defer close(derpMapCh) + clock := quartz.NewMock(t) + resumeTokenSigningKey, err := tailnet.GenerateResumeTokenSigningKey() + require.NoError(t, err) + resumeTokenProvider := tailnet.NewResumeTokenKeyProvider(resumeTokenSigningKey, clock, time.Hour) svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ Logger: logger, CoordPtr: &coordPtr, DERPMapUpdateFrequency: time.Millisecond, DERPMapFn: func() *tailcfg.DERPMap { return <-derpMapCh }, NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) {}, - ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), + ResumeTokenProvider: resumeTokenProvider, }) require.NoError(t, err) @@ -317,7 +322,6 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { fConn := newFakeTailnetConn() - clock := quartz.NewMock(t) newTickerTrap := clock.Trap().NewTicker("tailnetAPIConnector", "refreshToken") tickerResetTrap := clock.Trap().TickerReset("tailnetAPIConnector", "refreshToken", "reset") defer newTickerTrap.Close() @@ -327,8 +331,6 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { // Wait for the resume token to be fetched for the first time. trappedTicker := newTickerTrap.MustWait(ctx) trappedTicker.Release() - waiter := clock.Advance(trappedTicker.Duration) - waiter.MustWait(ctx) trappedReset := tickerResetTrap.MustWait(ctx) trappedReset.Release() originalResumeToken := uut.resumeToken.Token @@ -346,9 +348,6 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { // Since we failed the initial reconnect and we're definitely reconnected // now, the stored resume token should now be nil. require.Nil(t, uut.resumeToken) - // Continue to the next token fetch. - waiter = clock.Advance(trappedTicker.Duration) - waiter.MustWait(ctx) trappedReset = tickerResetTrap.MustWait(ctx) trappedReset.Release() require.NotNil(t, uut.resumeToken) diff --git a/tailnet/resume.go b/tailnet/resume.go index 9f7f9601df182..b9443064a37f9 100644 --- a/tailnet/resume.go +++ b/tailnet/resume.go @@ -116,13 +116,14 @@ func NewResumeTokenKeyProvider(key ResumeTokenSigningKey, clock quartz.Clock, ex type resumeTokenPayload struct { PeerID uuid.UUID `json:"sub"` - Expiry time.Time `json:"exp"` + Expiry int64 `json:"exp"` } func (p ResumeTokenKeyProvider) GenerateResumeToken(peerID uuid.UUID) (*proto.RefreshResumeTokenResponse, error) { + exp := p.clock.Now().Add(p.expiry) payload := resumeTokenPayload{ PeerID: peerID, - Expiry: p.clock.Now().Add(p.expiry), + Expiry: exp.Unix(), } payloadBytes, err := json.Marshal(payload) if err != nil { @@ -154,11 +155,11 @@ func (p ResumeTokenKeyProvider) GenerateResumeToken(peerID uuid.UUID) (*proto.Re return &proto.RefreshResumeTokenResponse{ Token: serialized, RefreshIn: durationpb.New(p.expiry / 2), - ExpiresAt: timestamppb.New(payload.Expiry), + ExpiresAt: timestamppb.New(exp), }, nil } -// VerifySignedToken parses a signed workspace app token with the given key and +// VerifyResumeToken parses a signed tailnet resume token with the given key and // returns the payload. If the token is invalid or expired, an error is // returned. func (p ResumeTokenKeyProvider) VerifyResumeToken(str string) (uuid.UUID, error) { @@ -186,7 +187,8 @@ func (p ResumeTokenKeyProvider) VerifyResumeToken(str string) (uuid.UUID, error) if err != nil { return uuid.Nil, xerrors.Errorf("unmarshal payload: %w", err) } - if tok.Expiry.Before(p.clock.Now()) { + exp := time.Unix(tok.Expiry, 0) + if exp.Before(p.clock.Now()) { return uuid.Nil, xerrors.New("signed resume token expired") } diff --git a/tailnet/resume_test.go b/tailnet/resume_test.go index c88d4e3710ab2..ce9437d5818cb 100644 --- a/tailnet/resume_test.go +++ b/tailnet/resume_test.go @@ -1,6 +1,7 @@ package tailnet_test import ( + "context" "encoding/hex" "testing" "time" @@ -22,8 +23,8 @@ func TestResumeTokenSigningKeyFromDatabase(t *testing.T) { assertRandomKey := func(t *testing.T, key tailnet.ResumeTokenSigningKey) { t.Helper() - assert.NotEqual(t, tailnet.ResumeTokenSigningKey{}, key, "key is empty") - assert.NotEqualValues(t, [64]byte{1}, key, "key is all 1s") + assert.NotEqual(t, tailnet.ResumeTokenSigningKey{}, key, "key should not be empty") + assert.NotEqualValues(t, [64]byte{1}, key, "key should not be all 1s") } t.Run("GenerateRetrieve", func(t *testing.T) { @@ -37,7 +38,7 @@ func TestResumeTokenSigningKeyFromDatabase(t *testing.T) { key2, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) require.NoError(t, err) - require.Equal(t, key1, key2, "keys are different") + require.Equal(t, key1, key2, "keys should not be different") }) t.Run("GetError", func(t *testing.T) { @@ -48,7 +49,6 @@ func TestResumeTokenSigningKeyFromDatabase(t *testing.T) { ctx := testutil.Context(t, testutil.WaitShort) _, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) - require.Error(t, err) require.ErrorIs(t, err, assert.AnError) }) @@ -61,7 +61,6 @@ func TestResumeTokenSigningKeyFromDatabase(t *testing.T) { ctx := testutil.Context(t, testutil.WaitShort) _, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) - require.Error(t, err) require.ErrorIs(t, err, assert.AnError) }) @@ -70,12 +69,21 @@ func TestResumeTokenSigningKeyFromDatabase(t *testing.T) { db := dbmock.NewMockStore(gomock.NewController(t)) db.EXPECT().GetCoordinatorResumeTokenSigningKey(gomock.Any()).Return("invalid", nil) - db.EXPECT().UpsertCoordinatorResumeTokenSigningKey(gomock.Any(), gomock.Any()).Return(nil) + + var storedKey tailnet.ResumeTokenSigningKey + db.EXPECT().UpsertCoordinatorResumeTokenSigningKey(gomock.Any(), gomock.Any()).Do(func(_ context.Context, value string) error { + keyBytes, err := hex.DecodeString(value) + require.NoError(t, err) + require.Len(t, keyBytes, len(storedKey)) + copy(storedKey[:], keyBytes) + return nil + }) ctx := testutil.Context(t, testutil.WaitShort) key, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) require.NoError(t, err) assertRandomKey(t, key) + require.Equal(t, storedKey, key, "key should match stored value") }) t.Run("LengthErrorShouldRegenerate", func(t *testing.T) { @@ -100,7 +108,6 @@ func TestResumeTokenSigningKeyFromDatabase(t *testing.T) { ctx := testutil.Context(t, testutil.WaitShort) _, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, db) - require.Error(t, err) require.ErrorContains(t, err, "is empty") }) } @@ -115,16 +122,14 @@ func TestResumeTokenKeyProvider(t *testing.T) { t.Parallel() id := uuid.New() - now := time.Now() clock := quartz.NewMock(t) - clock.Set(now) provider := tailnet.NewResumeTokenKeyProvider(key, clock, tailnet.DefaultResumeTokenExpiry) token, err := provider.GenerateResumeToken(id) require.NoError(t, err) require.NotNil(t, token) require.NotEmpty(t, token.Token) require.Equal(t, tailnet.DefaultResumeTokenExpiry/2, token.RefreshIn.AsDuration()) - require.WithinDuration(t, now.Add(tailnet.DefaultResumeTokenExpiry), token.ExpiresAt.AsTime(), time.Second) + require.WithinDuration(t, clock.Now().Add(tailnet.DefaultResumeTokenExpiry), token.ExpiresAt.AsTime(), time.Second) gotID, err := provider.VerifyResumeToken(token.Token) require.NoError(t, err) @@ -150,7 +155,6 @@ func TestResumeTokenKeyProvider(t *testing.T) { _ = clock.Advance(tailnet.DefaultResumeTokenExpiry + time.Second) _, err = provider.VerifyResumeToken(token.Token) - require.Error(t, err) require.ErrorContains(t, err, "expired") }) @@ -159,7 +163,6 @@ func TestResumeTokenKeyProvider(t *testing.T) { provider := tailnet.NewResumeTokenKeyProvider(key, quartz.NewMock(t), tailnet.DefaultResumeTokenExpiry) _, err := provider.VerifyResumeToken("invalid") - require.Error(t, err) require.ErrorContains(t, err, "parse JWS") }) @@ -175,7 +178,6 @@ func TestResumeTokenKeyProvider(t *testing.T) { provider := tailnet.NewResumeTokenKeyProvider(key, quartz.NewMock(t), tailnet.DefaultResumeTokenExpiry) _, err = provider.VerifyResumeToken(token.Token) - require.Error(t, err) require.ErrorContains(t, err, "verify JWS") }) } From 195fb04146cebf6dc735e56c26e1df31b100a3da Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 20 Aug 2024 06:23:16 +0000 Subject: [PATCH 6/6] PR comments 5 --- codersdk/workspacesdk/connector_internal_test.go | 16 +++++++--------- tailnet/resume_test.go | 4 +--- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index c517042f1622f..d56f45b4821b7 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -226,8 +226,7 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { // Fetch first token. We don't need to advance the clock since we use a // channel with a single item to immediately fetch. - trappedTicker := newTickerTrap.MustWait(ctx) - trappedTicker.Release() + newTickerTrap.MustWait(ctx).Release() // We call ticker.Reset after each token fetch to apply the refresh duration // requested by the server. trappedReset := tickerResetTrap.MustWait(ctx) @@ -250,10 +249,10 @@ func TestTailnetAPIConnector_ResumeToken(t *testing.T) { _ = wsConn.Close(websocket.StatusGoingAway, "test") // Wait for the resume token to be refreshed. - trappedTicker = newTickerTrap.MustWait(ctx) + trappedTicker := newTickerTrap.MustWait(ctx) + // Advance the clock slightly to ensure the new JWT is different. + clock.Advance(time.Second).MustWait(ctx) trappedTicker.Release() - waiter = clock.Advance(trappedTicker.Duration) - waiter.MustWait(ctx) trappedReset = tickerResetTrap.MustWait(ctx) trappedReset.Release() @@ -329,8 +328,7 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { uut.runConnector(fConn) // Wait for the resume token to be fetched for the first time. - trappedTicker := newTickerTrap.MustWait(ctx) - trappedTicker.Release() + newTickerTrap.MustWait(ctx).Release() trappedReset := tickerResetTrap.MustWait(ctx) trappedReset.Release() originalResumeToken := uut.resumeToken.Token @@ -343,11 +341,11 @@ func TestTailnetAPIConnector_ResumeTokenFailure(t *testing.T) { // Wait for the resume token to be refreshed, which indicates a successful // reconnect. - trappedTicker = newTickerTrap.MustWait(ctx) - trappedTicker.Release() + trappedTicker := newTickerTrap.MustWait(ctx) // Since we failed the initial reconnect and we're definitely reconnected // now, the stored resume token should now be nil. require.Nil(t, uut.resumeToken) + trappedTicker.Release() trappedReset = tickerResetTrap.MustWait(ctx) trappedReset.Release() require.NotNil(t, uut.resumeToken) diff --git a/tailnet/resume_test.go b/tailnet/resume_test.go index ce9437d5818cb..3f63887cbfef3 100644 --- a/tailnet/resume_test.go +++ b/tailnet/resume_test.go @@ -140,16 +140,14 @@ func TestResumeTokenKeyProvider(t *testing.T) { t.Parallel() id := uuid.New() - now := time.Now() clock := quartz.NewMock(t) - _ = clock.Set(now) provider := tailnet.NewResumeTokenKeyProvider(key, clock, tailnet.DefaultResumeTokenExpiry) token, err := provider.GenerateResumeToken(id) require.NoError(t, err) require.NotNil(t, token) require.NotEmpty(t, token.Token) require.Equal(t, tailnet.DefaultResumeTokenExpiry/2, token.RefreshIn.AsDuration()) - require.WithinDuration(t, now.Add(tailnet.DefaultResumeTokenExpiry), token.ExpiresAt.AsTime(), time.Second) + require.WithinDuration(t, clock.Now().Add(tailnet.DefaultResumeTokenExpiry), token.ExpiresAt.AsTime(), time.Second) // Advance time past expiry _ = clock.Advance(tailnet.DefaultResumeTokenExpiry + time.Second)