From abe8da697c7737bfe7aca45fba10f621506fad1d Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:07:30 +0100 Subject: [PATCH 01/23] [signal] add pprof and message size metrics (#3337) --- signal/cmd/run.go | 13 +++++++++++++ signal/metrics/app.go | 29 +++++++++++++++++++++++++++++ signal/server/signal.go | 2 ++ 3 files changed, 44 insertions(+) diff --git a/signal/cmd/run.go b/signal/cmd/run.go index 1bb2f1d0c..3a671a848 100644 --- a/signal/cmd/run.go +++ b/signal/cmd/run.go @@ -8,6 +8,8 @@ import ( "fmt" "net" "net/http" + // nolint:gosec + _ "net/http/pprof" "strings" "time" @@ -82,6 +84,8 @@ var ( RunE: func(cmd *cobra.Command, args []string) error { flag.Parse() + startPprof() + opts, certManager, err := getTLSConfigurations() if err != nil { return err @@ -170,6 +174,15 @@ var ( } ) +func startPprof() { + go func() { + log.Debugf("Starting pprof server on 127.0.0.1:6060") + if err := http.ListenAndServe("127.0.0.1:6060", nil); err != nil { + log.Fatalf("pprof server failed: %v", err) + } + }() +} + func getTLSConfigurations() ([]grpc.ServerOption, *autocert.Manager, error) { var ( err error diff --git a/signal/metrics/app.go b/signal/metrics/app.go index b3457cf96..e3b1c67cd 100644 --- a/signal/metrics/app.go +++ b/signal/metrics/app.go @@ -20,6 +20,8 @@ type AppMetrics struct { MessagesForwarded metric.Int64Counter MessageForwardFailures metric.Int64Counter MessageForwardLatency metric.Float64Histogram + + MessageSize metric.Int64Histogram } func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) { @@ -97,6 +99,16 @@ func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) { return nil, err } + messageSize, err := meter.Int64Histogram( + "message.size.bytes", + metric.WithUnit("bytes"), + metric.WithExplicitBucketBoundaries(getMessageSizeBucketBoundaries()...), + metric.WithDescription("Records the size of each message sent"), + ) + if err != nil { + return nil, err + } + return &AppMetrics{ Meter: meter, @@ -112,9 +124,26 @@ func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) { MessagesForwarded: messagesForwarded, MessageForwardFailures: messageForwardFailures, MessageForwardLatency: messageForwardLatency, + + MessageSize: messageSize, }, nil } +func getMessageSizeBucketBoundaries() []float64 { + return []float64{ + 100, + 250, + 500, + 1000, + 5000, + 10000, + 50000, + 100000, + 500000, + 1000000, + } +} + func getStandardBucketBoundaries() []float64 { return []float64{ 0.1, diff --git a/signal/server/signal.go b/signal/server/signal.go index abc1c367b..05cc43276 100644 --- a/signal/server/signal.go +++ b/signal/server/signal.go @@ -13,6 +13,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + gproto "google.golang.org/protobuf/proto" "github.com/netbirdio/netbird/signal/metrics" "github.com/netbirdio/netbird/signal/peer" @@ -175,4 +176,5 @@ func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedM // in milliseconds s.metrics.MessageForwardLatency.Record(ctx, float64(time.Since(start).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream))) s.metrics.MessagesForwarded.Add(ctx, 1) + s.metrics.MessageSize.Record(ctx, int64(gproto.Size(msg)), metric.WithAttributes(attribute.String(labelType, labelTypeMessage))) } From 4cdb2e533a2ef3d6dbefff390502d9cb45c07334 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 17 Feb 2025 21:43:12 +0300 Subject: [PATCH 02/23] [management] Refactor users to use store methods (#2917) * Refactor setup key handling to use store methods Signed-off-by: bcmmbaga * add lock to get account groups Signed-off-by: bcmmbaga * add check for regular user Signed-off-by: bcmmbaga * get only required groups for auto-group validation Signed-off-by: bcmmbaga * add account lock and return auto groups map on validation Signed-off-by: bcmmbaga * refactor account peers update Signed-off-by: bcmmbaga * Refactor groups to use store methods Signed-off-by: bcmmbaga * refactor GetGroupByID and add NewGroupNotFoundError Signed-off-by: bcmmbaga * fix tests Signed-off-by: bcmmbaga * Add AddPeer and RemovePeer methods to Group struct Signed-off-by: bcmmbaga * Preserve store engine in SqlStore transactions Signed-off-by: bcmmbaga * Run groups ops in transaction Signed-off-by: bcmmbaga * fix missing group removed from setup key activity Signed-off-by: bcmmbaga * fix merge Signed-off-by: bcmmbaga * Refactor posture checks to remove get and save account Signed-off-by: bcmmbaga * fix refactor Signed-off-by: bcmmbaga * fix tests Signed-off-by: bcmmbaga * fix merge Signed-off-by: bcmmbaga * fix sonar Signed-off-by: bcmmbaga * Change setup key log level to debug for missing group Signed-off-by: bcmmbaga * Retrieve modified peers once for group events Signed-off-by: bcmmbaga * Refactor policy get and save account to use store methods Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * Add tests Signed-off-by: bcmmbaga * Add tests Signed-off-by: bcmmbaga * Retrieve policy groups and posture checks once for validation Signed-off-by: bcmmbaga * Fix typo Signed-off-by: bcmmbaga * Add policy tests Signed-off-by: bcmmbaga * Refactor anyGroupHasPeers to retrieve all groups once Signed-off-by: bcmmbaga * Refactor dns settings to use store methods Signed-off-by: bcmmbaga * Add tests Signed-off-by: bcmmbaga * Add account locking and merge group deletion methods Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * Refactor name server groups to use store methods Signed-off-by: bcmmbaga * Add tests Signed-off-by: bcmmbaga * Add peer store methods Signed-off-by: bcmmbaga * Refactor ephemeral peers Signed-off-by: bcmmbaga * Add lock for peer store methods Signed-off-by: bcmmbaga * Refactor peer handlers Signed-off-by: bcmmbaga * Refactor peer to use store methods Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * Fix typo Signed-off-by: bcmmbaga * Add locks and remove log Signed-off-by: bcmmbaga * run peer ops in transaction Signed-off-by: bcmmbaga * remove duplicate store method Signed-off-by: bcmmbaga * fix peer fields updated after save Signed-off-by: bcmmbaga * add tests Signed-off-by: bcmmbaga * Use update strength and simplify check Signed-off-by: bcmmbaga * prevent changing ruleID when not empty Signed-off-by: bcmmbaga * prevent duplicate rules during updates Signed-off-by: bcmmbaga * fix tests Signed-off-by: bcmmbaga * fix lint Signed-off-by: bcmmbaga * Refactor auth middleware Signed-off-by: bcmmbaga * Refactor account methods and mock Signed-off-by: bcmmbaga * Refactor user and PAT handling Signed-off-by: bcmmbaga * Remove db query context and fix get user by id Signed-off-by: bcmmbaga * Fix database transaction locking issue Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * Use UTC time in test Signed-off-by: bcmmbaga * Add account locks Signed-off-by: bcmmbaga * Fix prevent users from creating PATs for other users Signed-off-by: bcmmbaga * Add tests Signed-off-by: bcmmbaga * Add store locks and prevent fetching setup keys peers when retrieving user peers with empty userID Signed-off-by: bcmmbaga * Add missing tests Signed-off-by: bcmmbaga * Refactor test names and remove duplicate TestPostgresql_SavePeerStatus Signed-off-by: bcmmbaga * Add account locks and remove redundant ephemeral check Signed-off-by: bcmmbaga * Retrieve all groups for peers and restrict groups for regular users Signed-off-by: bcmmbaga * Fix merge Signed-off-by: bcmmbaga * Fix merge Signed-off-by: bcmmbaga * fix merge Signed-off-by: bcmmbaga * fix store tests Signed-off-by: bcmmbaga * use account object to get validated peers Signed-off-by: bcmmbaga * Fix merge Signed-off-by: bcmmbaga * Improve peer performance Signed-off-by: bcmmbaga * Get account direct from store without buffer Signed-off-by: bcmmbaga * Add get peer groups tests Signed-off-by: bcmmbaga * Adjust benchmarks Signed-off-by: bcmmbaga * Adjust benchmarks Signed-off-by: bcmmbaga * [management] Update benchmark workflow (#3181) * update local benchmark expectations * update cloud expectations * Add status error for generic result error Signed-off-by: bcmmbaga * Use integrated validator direct Signed-off-by: bcmmbaga * update expectations * update expectations * update expectations * Refactor peer scheduler to retry every 3 seconds on errors Signed-off-by: bcmmbaga * update expectations * fix validator * fix validator * fix validator * update timeouts * Refactor ToGroupsInfo to process slices of groups Signed-off-by: bcmmbaga * update expectations * update expectations * update expectations * Bump integrations version Signed-off-by: bcmmbaga * Refactor GetValidatedPeers Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * go mod tidy Signed-off-by: bcmmbaga * Use peers and groups map for peers validation Signed-off-by: bcmmbaga * remove mysql from api benchmark tests * Fix merge Signed-off-by: bcmmbaga * Fix blocked db calls on user auto groups update Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * update expectations Signed-off-by: bcmmbaga * update expectations Signed-off-by: bcmmbaga * Skip user check for system initiated peer deletion Signed-off-by: bcmmbaga * Remove context in db calls Signed-off-by: bcmmbaga * update expectations Signed-off-by: bcmmbaga * [management] Improve group peer/resource counting (#3192) * Fix sonar Signed-off-by: bcmmbaga * Adjust bench expectations Signed-off-by: bcmmbaga * Rename GetAccountInfoFromPAT to GetTokenInfo Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * Remove global account lock for ListUsers Signed-off-by: bcmmbaga * build userinfo after updating users in db Signed-off-by: bcmmbaga * [management] Optimize user bulk deletion (#3315) * refactor building user infos Signed-off-by: bcmmbaga * fix tests Signed-off-by: bcmmbaga * remove unused code Signed-off-by: bcmmbaga * Refactor GetUsersFromAccount to return a map of UserInfo instead of a slice Signed-off-by: bcmmbaga * Export BuildUserInfosForAccount to account manager Signed-off-by: bcmmbaga * Fetch account user info once for bulk users save Signed-off-by: bcmmbaga * Update user deletion expectations Signed-off-by: bcmmbaga * Set max open conns for activity store Signed-off-by: bcmmbaga * Update bench expectations Signed-off-by: bcmmbaga --------- Signed-off-by: bcmmbaga --------- Signed-off-by: bcmmbaga Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Co-authored-by: Pascal Fischer Co-authored-by: Pedro Costa <550684+pnmcosta@users.noreply.github.com> --- management/client/rest/dns_test.go | 1 + management/server/account.go | 175 ++-- management/server/account_test.go | 7 +- management/server/activity/sqlite/sqlite.go | 2 + management/server/http/handler.go | 2 +- .../handlers/events/events_handler_test.go | 4 +- .../http/handlers/users/users_handler_test.go | 12 +- .../server/http/middleware/auth_middleware.go | 22 +- .../http/middleware/auth_middleware_test.go | 11 +- .../users_handler_benchmark_test.go | 50 +- management/server/mock_server/account_mock.go | 31 +- management/server/peer_test.go | 5 +- management/server/status/error.go | 17 +- management/server/store/sql_store.go | 184 +++- management/server/store/sql_store_test.go | 365 +++++++- management/server/store/store.go | 13 +- management/server/testdata/store.sql | 2 +- .../server/types/personal_access_token.go | 3 +- management/server/user.go | 877 ++++++++---------- management/server/user_test.go | 16 +- 20 files changed, 1080 insertions(+), 719 deletions(-) diff --git a/management/client/rest/dns_test.go b/management/client/rest/dns_test.go index 0d57d63d7..b2e0a0bee 100644 --- a/management/client/rest/dns_test.go +++ b/management/client/rest/dns_test.go @@ -260,6 +260,7 @@ func TestDNS_Integration(t *testing.T) { nsGroupReq := api.NameserverGroupRequest{ Description: "Test", Enabled: true, + Domains: []string{}, Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, Name: "test", Nameservers: []api.Nameserver{ diff --git a/management/server/account.go b/management/server/account.go index 2c62a2453..a0c6fd0b0 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -67,7 +67,7 @@ type AccountManager interface { SaveSetupKey(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error) CreateUser(ctx context.Context, accountID, initiatorUserID string, key *types.UserInfo) (*types.UserInfo, error) DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error - DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error + DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error InviteUser(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error ListSetupKeys(ctx context.Context, accountID, userID string) ([]*types.SetupKey, error) SaveUser(ctx context.Context, accountID, initiatorUserID string, update *types.User) (*types.UserInfo, error) @@ -79,7 +79,7 @@ type AccountManager interface { GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error) GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error - GetAccountFromPAT(ctx context.Context, pat string) (*types.Account, *types.User, *types.PersonalAccessToken, error) + GetPATInfo(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error) DeleteAccount(ctx context.Context, accountID, userID string) error MarkPATUsed(ctx context.Context, tokenID string) error GetUserByID(ctx context.Context, id string) (*types.User, error) @@ -96,7 +96,7 @@ type AccountManager interface { DeletePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error GetPAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) (*types.PersonalAccessToken, error) GetAllPATs(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) ([]*types.PersonalAccessToken, error) - GetUsersFromAccount(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error) + GetUsersFromAccount(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) GetGroup(ctx context.Context, accountId, groupID, userID string) (*types.Group, error) GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error) @@ -149,6 +149,7 @@ type AccountManager interface { GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error UpdateAccountPeers(ctx context.Context, accountID string) + BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) } type DefaultAccountManager struct { @@ -617,6 +618,12 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u if user.Role != types.UserRoleOwner { return status.Errorf(status.PermissionDenied, "user is not allowed to delete account. Only account owner can delete account") } + + userInfosMap, err := am.BuildUserInfosForAccount(ctx, accountID, userID, maps.Values(account.Users)) + if err != nil { + return status.Errorf(status.Internal, "failed to build user infos for account %s: %v", accountID, err) + } + for _, otherUser := range account.Users { if otherUser.IsServiceUser { continue @@ -626,13 +633,23 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u continue } - deleteUserErr := am.deleteRegularUser(ctx, account, userID, otherUser.Id) + userInfo, ok := userInfosMap[otherUser.Id] + if !ok { + return status.Errorf(status.NotFound, "user info not found for user %s", otherUser.Id) + } + + _, deleteUserErr := am.deleteRegularUser(ctx, accountID, userID, userInfo) if deleteUserErr != nil { return deleteUserErr } } - err = am.deleteRegularUser(ctx, account, userID, userID) + userInfo, ok := userInfosMap[userID] + if !ok { + return status.Errorf(status.NotFound, "user info not found for user %s", userID) + } + + _, err = am.deleteRegularUser(ctx, accountID, userID, userInfo) if err != nil { log.WithContext(ctx).Errorf("failed deleting user %s. error: %s", userID, err) return err @@ -689,20 +706,8 @@ func isNil(i idp.Manager) bool { // addAccountIDToIDPAppMeta update user's app metadata in idp manager func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, userID string, accountID string) error { if !isNil(am.idpManager) { - accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return err - } - cachedAccount := &types.Account{ - Id: accountID, - Users: make(map[string]*types.User), - } - for _, user := range accountUsers { - cachedAccount.Users[user.Id] = user - } - // user can be nil if it wasn't found (e.g., just created) - user, err := am.lookupUserInCache(ctx, userID, cachedAccount) + user, err := am.lookupUserInCache(ctx, userID, accountID) if err != nil { return err } @@ -778,10 +783,15 @@ func (am *DefaultAccountManager) lookupUserInCacheByEmail(ctx context.Context, e } // lookupUserInCache looks up user in the IdP cache and returns it. If the user wasn't found, the function returns nil -func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID string, account *types.Account) (*idp.UserData, error) { - users := make(map[string]userLoggedInOnce, len(account.Users)) +func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID string, accountID string) (*idp.UserData, error) { + accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + users := make(map[string]userLoggedInOnce, len(accountUsers)) // ignore service users and users provisioned by integrations than are never logged in - for _, user := range account.Users { + for _, user := range accountUsers { if user.IsServiceUser { continue } @@ -790,8 +800,8 @@ func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID s } users[user.Id] = userLoggedInOnce(!user.GetLastLogin().IsZero()) } - log.WithContext(ctx).Debugf("looking up user %s of account %s in cache", userID, account.Id) - userData, err := am.lookupCache(ctx, users, account.Id) + log.WithContext(ctx).Debugf("looking up user %s of account %s in cache", userID, accountID) + userData, err := am.lookupCache(ctx, users, accountID) if err != nil { return nil, err } @@ -804,13 +814,13 @@ func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID s // add extra check on external cache manager. We may get to this point when the user is not yet findable in IDP, // or it didn't have its metadata updated with am.addAccountIDToIDPAppMeta - user, err := account.FindUser(userID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { - log.WithContext(ctx).Errorf("failed finding user %s in account %s", userID, account.Id) + log.WithContext(ctx).Errorf("failed finding user %s in account %s", userID, accountID) return nil, err } - key := user.IntegrationReference.CacheKey(account.Id, userID) + key := user.IntegrationReference.CacheKey(accountID, userID) ud, err := am.externalCacheManager.Get(am.ctx, key) if err != nil { log.WithContext(ctx).Debugf("failed to get externalCache for key: %s, error: %s", key, err) @@ -1050,9 +1060,9 @@ func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, unlockAccount := am.Store.AcquireWriteLockByUID(ctx, domainAccountID) defer unlockAccount() - usersMap := make(map[string]*types.User) - usersMap[claims.UserId] = types.NewRegularUser(claims.UserId) - err := am.Store.SaveUsers(domainAccountID, usersMap) + newUser := types.NewRegularUser(claims.UserId) + newUser.AccountID = domainAccountID + err := am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser) if err != nil { return "", err } @@ -1075,12 +1085,7 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str return nil } - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return err - } - - user, err := am.lookupUserInCache(ctx, userID, account) + user, err := am.lookupUserInCache(ctx, userID, accountID) if err != nil { return err } @@ -1090,17 +1095,17 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str } if user.AppMetadata.WTPendingInvite != nil && *user.AppMetadata.WTPendingInvite { - log.WithContext(ctx).Infof("redeeming invite for user %s account %s", userID, account.Id) + log.WithContext(ctx).Infof("redeeming invite for user %s account %s", userID, accountID) // User has already logged in, meaning that IdP should have set wt_pending_invite to false. // Our job is to just reload cache. go func() { - _, err = am.refreshCache(ctx, account.Id) + _, err = am.refreshCache(ctx, accountID) if err != nil { - log.WithContext(ctx).Warnf("failed reloading cache when redeeming user %s under account %s", userID, account.Id) + log.WithContext(ctx).Warnf("failed reloading cache when redeeming user %s under account %s", userID, accountID) return } - log.WithContext(ctx).Debugf("user %s of account %s redeemed invite", user.ID, account.Id) - am.StoreEvent(ctx, userID, userID, account.Id, activity.UserJoined, nil) + log.WithContext(ctx).Debugf("user %s of account %s redeemed invite", user.ID, accountID) + am.StoreEvent(ctx, userID, userID, accountID, activity.UserJoined, nil) }() } @@ -1109,33 +1114,7 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str // MarkPATUsed marks a personal access token as used func (am *DefaultAccountManager) MarkPATUsed(ctx context.Context, tokenID string) error { - - user, err := am.Store.GetUserByTokenID(ctx, tokenID) - if err != nil { - return err - } - - account, err := am.Store.GetAccountByUser(ctx, user.Id) - if err != nil { - return err - } - - unlock := am.Store.AcquireWriteLockByUID(ctx, account.Id) - defer unlock() - - account, err = am.Store.GetAccountByUser(ctx, user.Id) - if err != nil { - return err - } - - pat, ok := account.Users[user.Id].PATs[tokenID] - if !ok { - return fmt.Errorf("token not found") - } - - pat.LastUsed = util.ToPtr(time.Now().UTC()) - - return am.Store.SaveAccount(ctx, account) + return am.Store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID) } // GetAccount returns an account associated with this account ID. @@ -1143,52 +1122,64 @@ func (am *DefaultAccountManager) GetAccount(ctx context.Context, accountID strin return am.Store.GetAccount(ctx, accountID) } -// GetAccountFromPAT returns Account and User associated with a personal access token -func (am *DefaultAccountManager) GetAccountFromPAT(ctx context.Context, token string) (*types.Account, *types.User, *types.PersonalAccessToken, error) { +// GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token. +func (am *DefaultAccountManager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) { + user, pat, err = am.extractPATFromToken(ctx, token) + if err != nil { + return nil, nil, "", "", err + } + + domain, category, err = am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID) + if err != nil { + return nil, nil, "", "", err + } + + return user, pat, domain, category, nil +} + +// extractPATFromToken validates the token structure and retrieves associated User and PAT. +func (am *DefaultAccountManager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) { if len(token) != types.PATLength { - return nil, nil, nil, fmt.Errorf("token has wrong length") + return nil, nil, fmt.Errorf("token has incorrect length") } prefix := token[:len(types.PATPrefix)] if prefix != types.PATPrefix { - return nil, nil, nil, fmt.Errorf("token has wrong prefix") + return nil, nil, fmt.Errorf("token has wrong prefix") } secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength] encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength] verificationChecksum, err := base62.Decode(encodedChecksum) if err != nil { - return nil, nil, nil, fmt.Errorf("token checksum decoding failed: %w", err) + return nil, nil, fmt.Errorf("token checksum decoding failed: %w", err) } secretChecksum := crc32.ChecksumIEEE([]byte(secret)) if secretChecksum != verificationChecksum { - return nil, nil, nil, fmt.Errorf("token checksum does not match") + return nil, nil, fmt.Errorf("token checksum does not match") } hashedToken := sha256.Sum256([]byte(token)) encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:]) - tokenID, err := am.Store.GetTokenIDByHashedToken(ctx, encodedHashedToken) + + var user *types.User + var pat *types.PersonalAccessToken + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken) + if err != nil { + return err + } + + user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID) + return err + }) if err != nil { - return nil, nil, nil, err + return nil, nil, err } - user, err := am.Store.GetUserByTokenID(ctx, tokenID) - if err != nil { - return nil, nil, nil, err - } - - account, err := am.Store.GetAccountByUser(ctx, user.Id) - if err != nil { - return nil, nil, nil, err - } - - pat := user.PATs[tokenID] - if pat == nil { - return nil, nil, nil, fmt.Errorf("personal access token not found") - } - - return account, user, pat, nil + return user, pat, nil } // GetAccountByID returns an account associated with this account ID. @@ -1334,7 +1325,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st return fmt.Errorf("error getting user peers: %w", err) } - updatedGroups, err := am.updateUserPeersInGroups(groupsMap, peers, addNewGroups, removeOldGroups) + updatedGroups, err := updateUserPeersInGroups(groupsMap, peers, addNewGroups, removeOldGroups) if err != nil { return fmt.Errorf("error modifying user peers in groups: %w", err) } diff --git a/management/server/account_test.go b/management/server/account_test.go index 1fc1ceb92..0a7f9119b 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -732,6 +732,7 @@ func TestAccountManager_GetAccountFromPAT(t *testing.T) { PATs: map[string]*types.PersonalAccessToken{ "tokenId": { ID: "tokenId", + UserID: "someUser", HashedToken: encodedHashedToken, }, }, @@ -745,14 +746,14 @@ func TestAccountManager_GetAccountFromPAT(t *testing.T) { Store: store, } - account, user, pat, err := am.GetAccountFromPAT(context.Background(), token) + user, pat, _, _, err := am.GetPATInfo(context.Background(), token) if err != nil { t.Fatalf("Error when getting Account from PAT: %s", err) } - assert.Equal(t, "account_id", account.Id) + assert.Equal(t, "account_id", user.AccountID) assert.Equal(t, "someUser", user.Id) - assert.Equal(t, account.Users["someUser"].PATs["tokenId"], pat) + assert.Equal(t, account.Users["someUser"].PATs["tokenId"].ID, pat.ID) } func TestDefaultAccountManager_MarkPATUsed(t *testing.T) { diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/sqlite/sqlite.go index 823e0b4ac..ffb863de9 100644 --- a/management/server/activity/sqlite/sqlite.go +++ b/management/server/activity/sqlite/sqlite.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "path/filepath" + "runtime" "time" _ "github.com/mattn/go-sqlite3" @@ -95,6 +96,7 @@ func NewSQLiteStore(ctx context.Context, dataDir string, encryptionKey string) ( if err != nil { return nil, err } + db.SetMaxOpenConns(runtime.NumCPU()) crypt, err := NewFieldEncrypt(encryptionKey) if err != nil { diff --git a/management/server/http/handler.go b/management/server/http/handler.go index cc2ad00b7..7ce09fffa 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -43,7 +43,7 @@ func NewAPIHandler(ctx context.Context, accountManager s.AccountManager, network ) authMiddleware := middleware.NewAuthMiddleware( - accountManager.GetAccountFromPAT, + accountManager.GetPATInfo, jwtValidator.ValidateAndParse, accountManager.MarkPATUsed, accountManager.CheckUserAccessByJWTGroups, diff --git a/management/server/http/handlers/events/events_handler_test.go b/management/server/http/handlers/events/events_handler_test.go index 17478aba3..fd603f289 100644 --- a/management/server/http/handlers/events/events_handler_test.go +++ b/management/server/http/handlers/events/events_handler_test.go @@ -32,8 +32,8 @@ func initEventsTestData(account string, events ...*activity.Event) *handler { GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { return claims.AccountId, claims.UserId, nil }, - GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) ([]*types.UserInfo, error) { - return make([]*types.UserInfo, 0), nil + GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) { + return make(map[string]*types.UserInfo), nil }, }, claimsExtractor: jwtclaims.NewClaimsExtractor( diff --git a/management/server/http/handlers/users/users_handler_test.go b/management/server/http/handlers/users/users_handler_test.go index 90081830a..ff77cedff 100644 --- a/management/server/http/handlers/users/users_handler_test.go +++ b/management/server/http/handlers/users/users_handler_test.go @@ -52,7 +52,7 @@ var usersTestAccount = &types.Account{ Issued: types.UserIssuedAPI, }, nonDeletableServiceUserID: { - Id: serviceUserID, + Id: nonDeletableServiceUserID, Role: "admin", IsServiceUser: true, NonDeletable: true, @@ -70,10 +70,10 @@ func initUsersTestData() *handler { GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) { return usersTestAccount.Users[id], nil }, - GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) ([]*types.UserInfo, error) { - users := make([]*types.UserInfo, 0) + GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) { + usersInfos := make(map[string]*types.UserInfo) for _, v := range usersTestAccount.Users { - users = append(users, &types.UserInfo{ + usersInfos[v.Id] = &types.UserInfo{ ID: v.Id, Role: string(v.Role), Name: "", @@ -81,9 +81,9 @@ func initUsersTestData() *handler { IsServiceUser: v.IsServiceUser, NonDeletable: v.NonDeletable, Issued: v.Issued, - }) + } } - return users, nil + return usersInfos, nil }, CreateUserFunc: func(_ context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error) { if userID != existingUserID { diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go index 182c30cf6..dcf73259a 100644 --- a/management/server/http/middleware/auth_middleware.go +++ b/management/server/http/middleware/auth_middleware.go @@ -19,8 +19,8 @@ import ( "github.com/netbirdio/netbird/management/server/types" ) -// GetAccountFromPATFunc function -type GetAccountFromPATFunc func(ctx context.Context, token string) (*types.Account, *types.User, *types.PersonalAccessToken, error) +// GetAccountInfoFromPATFunc function +type GetAccountInfoFromPATFunc func(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) // ValidateAndParseTokenFunc function type ValidateAndParseTokenFunc func(ctx context.Context, token string) (*jwt.Token, error) @@ -33,7 +33,7 @@ type CheckUserAccessByJWTGroupsFunc func(ctx context.Context, claims jwtclaims.A // AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens type AuthMiddleware struct { - getAccountFromPAT GetAccountFromPATFunc + getAccountInfoFromPAT GetAccountInfoFromPATFunc validateAndParseToken ValidateAndParseTokenFunc markPATUsed MarkPATUsedFunc checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc @@ -47,7 +47,7 @@ const ( ) // NewAuthMiddleware instance constructor -func NewAuthMiddleware(getAccountFromPAT GetAccountFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc, +func NewAuthMiddleware(getAccountInfoFromPAT GetAccountInfoFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc, markPATUsed MarkPATUsedFunc, checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc, claimsExtractor *jwtclaims.ClaimsExtractor, audience string, userIdClaim string) *AuthMiddleware { if userIdClaim == "" { @@ -55,7 +55,7 @@ func NewAuthMiddleware(getAccountFromPAT GetAccountFromPATFunc, validateAndParse } return &AuthMiddleware{ - getAccountFromPAT: getAccountFromPAT, + getAccountInfoFromPAT: getAccountInfoFromPAT, validateAndParseToken: validateAndParseToken, markPATUsed: markPATUsed, checkUserAccessByJWTGroups: checkUserAccessByJWTGroups, @@ -151,13 +151,11 @@ func (m *AuthMiddleware) verifyUserAccess(ctx context.Context, validatedToken *j // CheckPATFromRequest checks if the PAT is valid func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error { token, err := getTokenFromPATRequest(auth) - - // If an error occurs, call the error handler and return an error if err != nil { - return fmt.Errorf("Error extracting token: %w", err) + return fmt.Errorf("error extracting token: %w", err) } - account, user, pat, err := m.getAccountFromPAT(r.Context(), token) + user, pat, accDomain, accCategory, err := m.getAccountInfoFromPAT(r.Context(), token) if err != nil { return fmt.Errorf("invalid Token: %w", err) } @@ -172,9 +170,9 @@ func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Requ claimMaps := jwt.MapClaims{} claimMaps[m.userIDClaim] = user.Id - claimMaps[m.audience+jwtclaims.AccountIDSuffix] = account.Id - claimMaps[m.audience+jwtclaims.DomainIDSuffix] = account.Domain - claimMaps[m.audience+jwtclaims.DomainCategorySuffix] = account.DomainCategory + claimMaps[m.audience+jwtclaims.AccountIDSuffix] = user.AccountID + claimMaps[m.audience+jwtclaims.DomainIDSuffix] = accDomain + claimMaps[m.audience+jwtclaims.DomainCategorySuffix] = accCategory claimMaps[jwtclaims.IsToken] = true jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps) newRequest := r.WithContext(context.WithValue(r.Context(), jwtclaims.TokenUserProperty, jwtToken)) //nolint diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go index 41bdb7fc5..c1686ed44 100644 --- a/management/server/http/middleware/auth_middleware_test.go +++ b/management/server/http/middleware/auth_middleware_test.go @@ -34,7 +34,8 @@ var testAccount = &types.Account{ Domain: domain, Users: map[string]*types.User{ userID: { - Id: userID, + Id: userID, + AccountID: accountID, PATs: map[string]*types.PersonalAccessToken{ tokenID: { ID: tokenID, @@ -50,11 +51,11 @@ var testAccount = &types.Account{ }, } -func mockGetAccountFromPAT(_ context.Context, token string) (*types.Account, *types.User, *types.PersonalAccessToken, error) { +func mockGetAccountInfoFromPAT(_ context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) { if token == PAT { - return testAccount, testAccount.Users[userID], testAccount.Users[userID].PATs[tokenID], nil + return testAccount.Users[userID], testAccount.Users[userID].PATs[tokenID], testAccount.Domain, testAccount.DomainCategory, nil } - return nil, nil, nil, fmt.Errorf("PAT invalid") + return nil, nil, "", "", fmt.Errorf("PAT invalid") } func mockValidateAndParseToken(_ context.Context, token string) (*jwt.Token, error) { @@ -166,7 +167,7 @@ func TestAuthMiddleware_Handler(t *testing.T) { ) authMiddleware := NewAuthMiddleware( - mockGetAccountFromPAT, + mockGetAccountInfoFromPAT, mockValidateAndParseToken, mockMarkPATUsed, mockCheckUserAccessByJWTGroups, diff --git a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go index 549a51c0e..0baf76328 100644 --- a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go @@ -35,14 +35,14 @@ var benchCasesUsers = map[string]testing_tools.BenchmarkCase{ func BenchmarkUpdateUser(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Users - XS": {MinMsPerOpLocal: 700, MaxMsPerOpLocal: 1000, MinMsPerOpCICD: 1300, MaxMsPerOpCICD: 8000}, - "Users - S": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 4, MaxMsPerOpCICD: 50}, - "Users - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 250}, - "Users - L": {MinMsPerOpLocal: 60, MaxMsPerOpLocal: 100, MinMsPerOpCICD: 90, MaxMsPerOpCICD: 700}, - "Peers - L": {MinMsPerOpLocal: 300, MaxMsPerOpLocal: 500, MinMsPerOpCICD: 550, MaxMsPerOpCICD: 2400}, - "Groups - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 600, MinMsPerOpCICD: 750, MaxMsPerOpCICD: 5000}, - "Setup Keys - L": {MinMsPerOpLocal: 50, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 130, MaxMsPerOpCICD: 1000}, - "Users - XL": {MinMsPerOpLocal: 350, MaxMsPerOpLocal: 550, MinMsPerOpCICD: 650, MaxMsPerOpCICD: 3500}, + "Users - XS": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 160, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 310}, + "Users - S": {MinMsPerOpLocal: 0.3, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 15}, + "Users - M": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 3, MaxMsPerOpCICD: 20}, + "Users - L": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 50}, + "Peers - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 80, MaxMsPerOpCICD: 310}, + "Groups - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 120}, + "Setup Keys - L": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 50}, + "Users - XL": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 100, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 280}, } log.SetOutput(io.Discard) @@ -118,14 +118,14 @@ func BenchmarkGetOneUser(b *testing.B) { func BenchmarkGetAllUsers(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Users - XS": {MinMsPerOpLocal: 50, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 180}, - "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, - "Users - M": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 12, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, - "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, - "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, - "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, - "Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 140, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 200}, - "Users - XL": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 90}, + "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10}, + "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10}, + "Users - M": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 15}, + "Users - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 50}, + "Peers - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 55}, + "Groups - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55}, + "Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55}, + "Users - XL": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300}, } log.SetOutput(io.Discard) @@ -141,7 +141,7 @@ func BenchmarkGetAllUsers(b *testing.B) { b.ResetTimer() start := time.Now() for i := 0; i < b.N; i++ { - req := testing_tools.BuildRequest(b, nil, http.MethodGet, "/api/setup-keys", testing_tools.TestAdminId) + req := testing_tools.BuildRequest(b, nil, http.MethodGet, "/api/users", testing_tools.TestAdminId) apiHandler.ServeHTTP(recorder, req) } @@ -152,14 +152,14 @@ func BenchmarkGetAllUsers(b *testing.B) { func BenchmarkDeleteUsers(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Users - XS": {MinMsPerOpLocal: 1000, MaxMsPerOpLocal: 1600, MinMsPerOpCICD: 1900, MaxMsPerOpCICD: 11000}, - "Users - S": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 200}, - "Users - M": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 15, MaxMsPerOpCICD: 230}, - "Users - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 45, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 190}, - "Peers - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 600, MinMsPerOpCICD: 650, MaxMsPerOpCICD: 1800}, - "Groups - L": {MinMsPerOpLocal: 600, MaxMsPerOpLocal: 800, MinMsPerOpCICD: 1200, MaxMsPerOpCICD: 7500}, - "Setup Keys - L": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 40, MaxMsPerOpCICD: 600}, - "Users - XL": {MinMsPerOpLocal: 50, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 80, MaxMsPerOpCICD: 400}, + "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Users - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Users - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, } log.SetOutput(io.Discard) diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index c8e42d20a..b20eb87bb 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -53,8 +53,8 @@ type MockAccountManager struct { SavePolicyFunc func(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error) DeletePolicyFunc func(ctx context.Context, accountID, policyID, userID string) error ListPoliciesFunc func(ctx context.Context, accountID, userID string) ([]*types.Policy, error) - GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error) - GetAccountFromPATFunc func(ctx context.Context, pat string) (*types.Account, *types.User, *types.PersonalAccessToken, error) + GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) + GetPATInfoFunc func(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error) MarkPATUsedFunc func(ctx context.Context, pat string) error UpdatePeerMetaFunc func(ctx context.Context, peerID string, meta nbpeer.PeerSystemMeta) error UpdatePeerFunc func(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) @@ -69,7 +69,7 @@ type MockAccountManager struct { SaveOrAddUserFunc func(ctx context.Context, accountID, userID string, user *types.User, addIfNotExists bool) (*types.UserInfo, error) SaveOrAddUsersFunc func(ctx context.Context, accountID, initiatorUserID string, update []*types.User, addIfNotExists bool) ([]*types.UserInfo, error) DeleteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error - DeleteRegularUsersFunc func(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error + DeleteRegularUsersFunc func(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error CreatePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) DeletePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) error GetPATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) (*types.PersonalAccessToken, error) @@ -110,6 +110,7 @@ type MockAccountManager struct { GetUserByIDFunc func(ctx context.Context, id string) (*types.User, error) GetAccountSettingsFunc func(ctx context.Context, accountID string, userID string) (*types.Settings, error) DeleteSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) error + BuildUserInfosForAccountFunc func(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) } func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) { @@ -165,7 +166,7 @@ func (am *MockAccountManager) GetAllGroups(ctx context.Context, accountID, userI } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface -func (am *MockAccountManager) GetUsersFromAccount(ctx context.Context, accountID string, userID string) ([]*types.UserInfo, error) { +func (am *MockAccountManager) GetUsersFromAccount(ctx context.Context, accountID string, userID string) (map[string]*types.UserInfo, error) { if am.GetUsersFromAccountFunc != nil { return am.GetUsersFromAccountFunc(ctx, accountID, userID) } @@ -238,12 +239,12 @@ func (am *MockAccountManager) MarkPeerConnected(ctx context.Context, peerKey str return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented") } -// GetAccountFromPAT mock implementation of GetAccountFromPAT from server.AccountManager interface -func (am *MockAccountManager) GetAccountFromPAT(ctx context.Context, pat string) (*types.Account, *types.User, *types.PersonalAccessToken, error) { - if am.GetAccountFromPATFunc != nil { - return am.GetAccountFromPATFunc(ctx, pat) +// GetPATInfo mock implementation of GetPATInfo from server.AccountManager interface +func (am *MockAccountManager) GetPATInfo(ctx context.Context, pat string) (*types.User, *types.PersonalAccessToken, string, string, error) { + if am.GetPATInfoFunc != nil { + return am.GetPATInfoFunc(ctx, pat) } - return nil, nil, nil, status.Errorf(codes.Unimplemented, "method GetAccountFromPAT is not implemented") + return nil, nil, "", "", status.Errorf(codes.Unimplemented, "method GetPATInfo is not implemented") } // DeleteAccount mock implementation of DeleteAccount from server.AccountManager interface @@ -550,9 +551,9 @@ func (am *MockAccountManager) DeleteUser(ctx context.Context, accountID string, } // DeleteRegularUsers mocks DeleteRegularUsers of the AccountManager interface -func (am *MockAccountManager) DeleteRegularUsers(ctx context.Context, accountID string, initiatorUserID string, targetUserIDs []string) error { +func (am *MockAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error { if am.DeleteRegularUsersFunc != nil { - return am.DeleteRegularUsersFunc(ctx, accountID, initiatorUserID, targetUserIDs) + return am.DeleteRegularUsersFunc(ctx, accountID, initiatorUserID, targetUserIDs, userInfos) } return status.Errorf(codes.Unimplemented, "method DeleteRegularUsers is not implemented") } @@ -849,3 +850,11 @@ func (am *MockAccountManager) GetPeerGroups(ctx context.Context, accountID, peer } return nil, status.Errorf(codes.Unimplemented, "method GetPeerGroups is not implemented") } + +// BuildUserInfosForAccount mocks BuildUserInfosForAccount of the AccountManager interface +func (am *MockAccountManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) { + if am.BuildUserInfosForAccountFunc != nil { + return am.BuildUserInfosForAccountFunc(ctx, accountID, initiatorUserID, accountUsers) + } + return nil, status.Errorf(codes.Unimplemented, "method BuildUserInfosForAccount is not implemented") +} diff --git a/management/server/peer_test.go b/management/server/peer_test.go index a0417c996..6894d092d 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + nbAccount "github.com/netbirdio/netbird/management/server/account" "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -28,7 +29,6 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/proto" - nbAccount "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" @@ -1554,7 +1554,8 @@ func TestPeerAccountPeersUpdate(t *testing.T) { // Adding peer to group linked with policy should update account peers and send peer update t.Run("adding peer to group linked with policy", func(t *testing.T) { _, err = manager.SavePolicy(context.Background(), account.Id, userID, &types.Policy{ - Enabled: true, + AccountID: account.Id, + Enabled: true, Rules: []*types.PolicyRule{ { Enabled: true, diff --git a/management/server/status/error.go b/management/server/status/error.go index 7e384922d..96b103183 100644 --- a/management/server/status/error.go +++ b/management/server/status/error.go @@ -93,7 +93,7 @@ func NewPeerNotPartOfAccountError() error { // NewUserNotFoundError creates a new Error with NotFound type for a missing user func NewUserNotFoundError(userKey string) error { - return Errorf(NotFound, "user not found: %s", userKey) + return Errorf(NotFound, "user: %s not found", userKey) } // NewPeerNotRegisteredError creates a new Error with NotFound type for a missing peer @@ -191,3 +191,18 @@ func NewResourceNotPartOfNetworkError(resourceID, networkID string) error { func NewRouterNotPartOfNetworkError(routerID, networkID string) error { return Errorf(BadRequest, "router %s is not part of the network %s", routerID, networkID) } + +// NewServiceUserRoleInvalidError creates a new Error with InvalidArgument type for creating a service user with owner role +func NewServiceUserRoleInvalidError() error { + return Errorf(InvalidArgument, "can't create a service user with owner role") +} + +// NewOwnerDeletePermissionError creates a new Error with PermissionDenied type for attempting +// to delete a user with the owner role. +func NewOwnerDeletePermissionError() error { + return Errorf(PermissionDenied, "can't delete a user with the owner role") +} + +func NewPATNotFoundError(patID string) error { + return Errorf(NotFound, "PAT: %s not found", patID) +} diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 2179f0754..6a6753595 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -15,6 +15,7 @@ import ( "sync" "time" + "github.com/netbirdio/netbird/management/server/util" log "github.com/sirupsen/logrus" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -414,24 +415,16 @@ func (s *SqlStore) SavePeerLocation(ctx context.Context, lockStrength LockingStr } // SaveUsers saves the given list of users to the database. -// It updates existing users if a conflict occurs. -func (s *SqlStore) SaveUsers(accountID string, users map[string]*types.User) error { - usersToSave := make([]types.User, 0, len(users)) - for _, user := range users { - user.AccountID = accountID - for id, pat := range user.PATs { - pat.ID = id - user.PATsG = append(user.PATsG, *pat) - } - usersToSave = append(usersToSave, *user) - } - err := s.db.Session(&gorm.Session{FullSaveAssociations: true}). - Clauses(clause.OnConflict{UpdateAll: true}). - Create(&usersToSave).Error - if err != nil { - return status.Errorf(status.Internal, "failed to save users to store: %v", err) +func (s *SqlStore) SaveUsers(ctx context.Context, lockStrength LockingStrength, users []*types.User) error { + if len(users) == 0 { + return nil } + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&users) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to save users to store: %s", result.Error) + return status.Errorf(status.Internal, "failed to save users to store") + } return nil } @@ -439,7 +432,8 @@ func (s *SqlStore) SaveUsers(accountID string, users map[string]*types.User) err func (s *SqlStore) SaveUser(ctx context.Context, lockStrength LockingStrength, user *types.User) error { result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(user) if result.Error != nil { - return status.Errorf(status.Internal, "failed to save user to store: %v", result.Error) + log.WithContext(ctx).Errorf("failed to save user to store: %s", result.Error) + return status.Errorf(status.Internal, "failed to save user to store") } return nil } @@ -526,30 +520,17 @@ func (s *SqlStore) GetTokenIDByHashedToken(ctx context.Context, hashedToken stri return token.ID, nil } -func (s *SqlStore) GetUserByTokenID(ctx context.Context, tokenID string) (*types.User, error) { - var token types.PersonalAccessToken - result := s.db.First(&token, idQueryCondition, tokenID) +func (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) { + var user types.User + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Joins("JOIN personal_access_tokens ON personal_access_tokens.user_id = users.id"). + Where("personal_access_tokens.id = ?", patID).First(&user) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + return nil, status.NewPATNotFoundError(patID) } - log.WithContext(ctx).Errorf("error when getting token from the store: %s", result.Error) - return nil, status.NewGetAccountFromStoreError(result.Error) - } - - if token.UserID == "" { - return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") - } - - var user types.User - result = s.db.Preload("PATsG").First(&user, idQueryCondition, token.UserID) - if result.Error != nil { - return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") - } - - user.PATs = make(map[string]*types.PersonalAccessToken, len(user.PATsG)) - for _, pat := range user.PATsG { - user.PATs[pat.ID] = pat.Copy() + log.WithContext(ctx).Errorf("failed to get token user from the store: %s", result.Error) + return nil, status.NewGetUserFromStoreError() } return &user, nil @@ -557,8 +538,7 @@ func (s *SqlStore) GetUserByTokenID(ctx context.Context, tokenID string) (*types func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) { var user types.User - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Preload(clause.Associations).First(&user, idQueryCondition, userID) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&user, idQueryCondition, userID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewUserNotFoundError(userID) @@ -569,6 +549,25 @@ func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStre return &user, nil } +func (s *SqlStore) DeleteUser(ctx context.Context, lockStrength LockingStrength, accountID, userID string) error { + err := s.db.Transaction(func(tx *gorm.DB) error { + result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}). + Delete(&types.PersonalAccessToken{}, "user_id = ?", userID) + if result.Error != nil { + return result.Error + } + + return tx.Clauses(clause.Locking{Strength: string(lockStrength)}). + Delete(&types.User{}, accountAndIDQueryCondition, accountID, userID).Error + }) + if err != nil { + log.WithContext(ctx).Errorf("failed to delete user from the store: %s", err) + return status.Errorf(status.Internal, "failed to delete user from store") + } + + return nil +} + func (s *SqlStore) GetAccountUsers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.User, error) { var users []*types.User result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&users, accountIDCondition, accountID) @@ -899,6 +898,20 @@ func (s *SqlStore) GetAccountSettings(ctx context.Context, lockStrength LockingS return accountSettings.Settings, nil } +func (s *SqlStore) GetAccountCreatedBy(ctx context.Context, lockStrength LockingStrength, accountID string) (string, error) { + var createdBy string + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). + Select("created_by").First(&createdBy, idQueryCondition, accountID) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return "", status.NewAccountNotFoundError(accountID) + } + return "", status.NewGetAccountFromStoreError(result.Error) + } + + return createdBy, nil +} + // SaveUserLastLogin stores the last login time for a user in DB. func (s *SqlStore) SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error { var user types.User @@ -2053,3 +2066,94 @@ func (s *SqlStore) DeleteNetworkResource(ctx context.Context, lockStrength Locki return nil } + +// GetPATByHashedToken returns a PersonalAccessToken by its hashed token. +func (s *SqlStore) GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error) { + var pat types.PersonalAccessToken + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&pat, "hashed_token = ?", hashedToken) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, status.NewPATNotFoundError(hashedToken) + } + log.WithContext(ctx).Errorf("failed to get pat by hash from the store: %s", result.Error) + return nil, status.Errorf(status.Internal, "failed to get pat by hash from store") + } + + return &pat, nil +} + +// GetPATByID retrieves a personal access token by its ID and user ID. +func (s *SqlStore) GetPATByID(ctx context.Context, lockStrength LockingStrength, userID string, patID string) (*types.PersonalAccessToken, error) { + var pat types.PersonalAccessToken + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + First(&pat, "id = ? AND user_id = ?", patID, userID) + if err := result.Error; err != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, status.NewPATNotFoundError(patID) + } + log.WithContext(ctx).Errorf("failed to get pat from the store: %s", err) + return nil, status.Errorf(status.Internal, "failed to get pat from store") + } + + return &pat, nil +} + +// GetUserPATs retrieves personal access tokens for a user. +func (s *SqlStore) GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error) { + var pats []*types.PersonalAccessToken + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&pats, "user_id = ?", userID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to get user pat's from the store: %s", err) + return nil, status.Errorf(status.Internal, "failed to get user pat's from store") + } + + return pats, nil +} + +// MarkPATUsed marks a personal access token as used. +func (s *SqlStore) MarkPATUsed(ctx context.Context, lockStrength LockingStrength, patID string) error { + patCopy := types.PersonalAccessToken{ + LastUsed: util.ToPtr(time.Now().UTC()), + } + + fieldsToUpdate := []string{"last_used"} + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Select(fieldsToUpdate). + Where(idQueryCondition, patID).Updates(&patCopy) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to mark pat as used: %s", result.Error) + return status.Errorf(status.Internal, "failed to mark pat as used") + } + + if result.RowsAffected == 0 { + return status.NewPATNotFoundError(patID) + } + + return nil +} + +// SavePAT saves a personal access token to the database. +func (s *SqlStore) SavePAT(ctx context.Context, lockStrength LockingStrength, pat *types.PersonalAccessToken) error { + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(pat) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to save pat to the store: %s", err) + return status.Errorf(status.Internal, "failed to save pat to store") + } + + return nil +} + +// DeletePAT deletes a personal access token from the database. +func (s *SqlStore) DeletePAT(ctx context.Context, lockStrength LockingStrength, userID, patID string) error { + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Delete(&types.PersonalAccessToken{}, "user_id = ? AND id = ?", userID, patID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to delete pat from the store: %s", err) + return status.Errorf(status.Internal, "failed to delete pat from store") + } + + if result.RowsAffected == 0 { + return status.NewPATNotFoundError(patID) + } + + return nil +} diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 9350da1c8..4dcdadf44 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -627,29 +627,6 @@ func TestSqlite_GetTokenIDByHashedToken(t *testing.T) { require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } -func TestSqlite_GetUserByTokenID(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("The SQLite store is not properly supported by Windows yet") - } - - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - - user, err := store.GetUserByTokenID(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, user.PATs[id].ID) - - _, err = store.GetUserByTokenID(context.Background(), "non-existing-id") - require.Error(t, err) - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") -} - func TestMigrate(t *testing.T) { if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { t.Skip("skip CI tests on darwin and windows") @@ -962,23 +939,6 @@ func TestPostgresql_GetTokenIDByHashedToken(t *testing.T) { require.Equal(t, id, token) } -func TestPostgresql_GetUserByTokenID(t *testing.T) { - if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { - t.Skip("skip CI tests on darwin and windows") - } - - t.Setenv("NETBIRD_STORE_ENGINE", string(PostgresStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - - user, err := store.GetUserByTokenID(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, user.PATs[id].ID) -} - func TestSqlite_GetTakenIPs(t *testing.T) { t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) @@ -1182,7 +1142,7 @@ func TestSqlite_CreateAndGetObjectInTransaction(t *testing.T) { assert.NoError(t, err) } -func TestSqlite_GetAccoundUsers(t *testing.T) { +func TestSqlStore_GetAccountUsers(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) t.Cleanup(cleanup) if err != nil { @@ -2915,3 +2875,326 @@ func TestSqlStore_DatabaseBlocking(t *testing.T) { t.Logf("Test completed") } + +func TestSqlStore_GetAccountCreatedBy(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + accountID string + expectError bool + createdBy string + }{ + { + name: "existing account ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + expectError: false, + createdBy: "edafee4e-63fb-11ec-90d6-0242ac120003", + }, + { + name: "non-existing account ID", + accountID: "nonexistent", + expectError: true, + }, + { + name: "empty account ID", + accountID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + createdBy, err := store.GetAccountCreatedBy(context.Background(), LockingStrengthShare, tt.accountID) + if tt.expectError { + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Empty(t, createdBy) + } else { + require.NoError(t, err) + require.NotNil(t, createdBy) + require.Equal(t, tt.createdBy, createdBy) + } + }) + } + +} + +func TestSqlStore_GetUserByUserID(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + userID string + expectError bool + }{ + { + name: "retrieve existing user", + userID: "edafee4e-63fb-11ec-90d6-0242ac120003", + expectError: false, + }, + { + name: "retrieve non-existing user", + userID: "non-existing", + expectError: true, + }, + { + name: "retrieve with empty user ID", + userID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, tt.userID) + if tt.expectError { + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Nil(t, user) + } else { + require.NoError(t, err) + require.NotNil(t, user) + require.Equal(t, tt.userID, user.Id) + } + }) + } +} + +func TestSqlStore_GetUserByPATID(t *testing.T) { + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) + + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + user, err := store.GetUserByPATID(context.Background(), LockingStrengthShare, id) + require.NoError(t, err) + require.Equal(t, "f4f6d672-63fb-11ec-90d6-0242ac120003", user.Id) +} + +func TestSqlStore_SaveUser(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + user := &types.User{ + Id: "user-id", + AccountID: accountID, + Role: types.UserRoleAdmin, + IsServiceUser: false, + AutoGroups: []string{"groupA", "groupB"}, + Blocked: false, + LastLogin: util.ToPtr(time.Now().UTC()), + CreatedAt: time.Now().UTC().Add(-time.Hour), + Issued: types.UserIssuedIntegration, + } + err = store.SaveUser(context.Background(), LockingStrengthUpdate, user) + require.NoError(t, err) + + saveUser, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, user.Id) + require.NoError(t, err) + require.Equal(t, user.Id, saveUser.Id) + require.Equal(t, user.AccountID, saveUser.AccountID) + require.Equal(t, user.Role, saveUser.Role) + require.Equal(t, user.AutoGroups, saveUser.AutoGroups) + require.WithinDurationf(t, user.GetLastLogin(), saveUser.LastLogin.UTC(), time.Millisecond, "LastLogin should be equal") + require.WithinDurationf(t, user.CreatedAt, saveUser.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal") + require.Equal(t, user.Issued, saveUser.Issued) + require.Equal(t, user.Blocked, saveUser.Blocked) + require.Equal(t, user.IsServiceUser, saveUser.IsServiceUser) +} + +func TestSqlStore_SaveUsers(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountUsers, 2) + + users := []*types.User{ + { + Id: "user-1", + AccountID: accountID, + Issued: "api", + AutoGroups: []string{"groupA", "groupB"}, + }, + { + Id: "user-2", + AccountID: accountID, + Issued: "integration", + AutoGroups: []string{"groupA"}, + }, + } + err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users) + require.NoError(t, err) + + accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountUsers, 4) +} + +func TestSqlStore_DeleteUser(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + + err = store.DeleteUser(context.Background(), LockingStrengthUpdate, accountID, userID) + require.NoError(t, err) + + user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, userID) + require.Error(t, err) + require.Nil(t, user) + + userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, userID) + require.NoError(t, err) + require.Len(t, userPATs, 0) +} + +func TestSqlStore_GetPATByID(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + + tests := []struct { + name string + patID string + expectError bool + }{ + { + name: "retrieve existing PAT", + patID: "9dj38s35-63fb-11ec-90d6-0242ac120003", + expectError: false, + }, + { + name: "retrieve non-existing PAT", + patID: "non-existing", + expectError: true, + }, + { + name: "retrieve with empty PAT ID", + patID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, tt.patID) + if tt.expectError { + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Nil(t, pat) + } else { + require.NoError(t, err) + require.NotNil(t, pat) + require.Equal(t, tt.patID, pat.ID) + } + }) + } +} + +func TestSqlStore_GetUserPATs(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, "f4f6d672-63fb-11ec-90d6-0242ac120003") + require.NoError(t, err) + require.Len(t, userPATs, 1) +} + +func TestSqlStore_GetPATByHashedToken(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + pat, err := store.GetPATByHashedToken(context.Background(), LockingStrengthShare, "SoMeHaShEdToKeN") + require.NoError(t, err) + require.Equal(t, "9dj38s35-63fb-11ec-90d6-0242ac120003", pat.ID) +} + +func TestSqlStore_MarkPATUsed(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + patID := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + err = store.MarkPATUsed(context.Background(), LockingStrengthUpdate, patID) + require.NoError(t, err) + + pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID) + require.NoError(t, err) + now := time.Now().UTC() + require.WithinRange(t, pat.LastUsed.UTC(), now.Add(-15*time.Second), now, "LastUsed should be within 1 second of now") +} + +func TestSqlStore_SavePAT(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "edafee4e-63fb-11ec-90d6-0242ac120003" + + pat := &types.PersonalAccessToken{ + ID: "pat-id", + UserID: userID, + Name: "token", + HashedToken: "SoMeHaShEdToKeN", + ExpirationDate: util.ToPtr(time.Now().UTC().Add(12 * time.Hour)), + CreatedBy: userID, + CreatedAt: time.Now().UTC().Add(time.Hour), + LastUsed: util.ToPtr(time.Now().UTC().Add(-15 * time.Minute)), + } + err = store.SavePAT(context.Background(), LockingStrengthUpdate, pat) + require.NoError(t, err) + + savePAT, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, pat.ID) + require.NoError(t, err) + require.Equal(t, pat.ID, savePAT.ID) + require.Equal(t, pat.UserID, savePAT.UserID) + require.Equal(t, pat.HashedToken, savePAT.HashedToken) + require.Equal(t, pat.CreatedBy, savePAT.CreatedBy) + require.WithinDurationf(t, pat.GetExpirationDate(), savePAT.ExpirationDate.UTC(), time.Millisecond, "ExpirationDate should be equal") + require.WithinDurationf(t, pat.CreatedAt, savePAT.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal") + require.WithinDurationf(t, pat.GetLastUsed(), savePAT.LastUsed.UTC(), time.Millisecond, "LastUsed should be equal") +} + +func TestSqlStore_DeletePAT(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + patID := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + err = store.DeletePAT(context.Background(), LockingStrengthUpdate, userID, patID) + require.NoError(t, err) + + pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID) + require.Error(t, err) + require.Nil(t, pat) +} diff --git a/management/server/store/store.go b/management/server/store/store.go index 4b4dcfb4f..6d3a409e6 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -59,21 +59,30 @@ type Store interface { GetAccountIDByPrivateDomain(ctx context.Context, lockStrength LockingStrength, domain string) (string, error) GetAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Settings, error) GetAccountDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.DNSSettings, error) + GetAccountCreatedBy(ctx context.Context, lockStrength LockingStrength, accountID string) (string, error) SaveAccount(ctx context.Context, account *types.Account) error DeleteAccount(ctx context.Context, account *types.Account) error UpdateAccountDomainAttributes(ctx context.Context, accountID string, domain string, category string, isPrimaryDomain bool) error SaveDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.DNSSettings) error - GetUserByTokenID(ctx context.Context, tokenID string) (*types.User, error) + GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) GetAccountUsers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.User, error) - SaveUsers(accountID string, users map[string]*types.User) error + SaveUsers(ctx context.Context, lockStrength LockingStrength, users []*types.User) error SaveUser(ctx context.Context, lockStrength LockingStrength, user *types.User) error SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error + DeleteUser(ctx context.Context, lockStrength LockingStrength, accountID, userID string) error GetTokenIDByHashedToken(ctx context.Context, secret string) (string, error) DeleteHashedPAT2TokenIDIndex(hashedToken string) error DeleteTokenID2UserIDIndex(tokenID string) error + GetPATByID(ctx context.Context, lockStrength LockingStrength, userID, patID string) (*types.PersonalAccessToken, error) + GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error) + GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error) + MarkPATUsed(ctx context.Context, lockStrength LockingStrength, patID string) error + SavePAT(ctx context.Context, strength LockingStrength, pat *types.PersonalAccessToken) error + DeletePAT(ctx context.Context, strength LockingStrength, userID, patID string) error + GetAccountGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Group, error) GetResourceGroups(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) ([]*types.Group, error) GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error) diff --git a/management/server/testdata/store.sql b/management/server/testdata/store.sql index 1c0767bde..41b8fa2f7 100644 --- a/management/server/testdata/store.sql +++ b/management/server/testdata/store.sql @@ -37,7 +37,7 @@ CREATE INDEX `idx_network_resources_id` ON `network_resources`(`id`); CREATE INDEX `idx_networks_id` ON `networks`(`id`); CREATE INDEX `idx_networks_account_id` ON `networks`(`account_id`); -INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 16:03:06.778746+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); +INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','edafee4e-63fb-11ec-90d6-0242ac120003','2024-10-02 16:03:06.778746+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); INSERT INTO "groups" VALUES('cs1tnh0hhcjnqoiuebeg','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','[]',0,''); INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cs1tnh0hhcjnqoiuebeg"]',0,0); INSERT INTO users VALUES('a23efe53-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','owner',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,''); diff --git a/management/server/types/personal_access_token.go b/management/server/types/personal_access_token.go index ff157fcc6..0aa6b152b 100644 --- a/management/server/types/personal_access_token.go +++ b/management/server/types/personal_access_token.go @@ -75,7 +75,7 @@ type PersonalAccessTokenGenerated struct { // CreateNewPAT will generate a new PersonalAccessToken that can be assigned to a User. // Additionally, it will return the token in plain text once, to give to the user and only save a hashed version -func CreateNewPAT(name string, expirationInDays int, createdBy string) (*PersonalAccessTokenGenerated, error) { +func CreateNewPAT(name string, expirationInDays int, targetID, createdBy string) (*PersonalAccessTokenGenerated, error) { hashedToken, plainToken, err := generateNewToken() if err != nil { return nil, err @@ -84,6 +84,7 @@ func CreateNewPAT(name string, expirationInDays int, createdBy string) (*Persona return &PersonalAccessTokenGenerated{ PersonalAccessToken: PersonalAccessToken{ ID: xid.New().String(), + UserID: targetID, Name: name, HashedToken: hashedToken, ExpirationDate: util.ToPtr(currentTime.AddDate(0, 0, expirationInDays)), diff --git a/management/server/user.go b/management/server/user.go index 17770a423..6ba9b68d3 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -4,13 +4,10 @@ import ( "context" "errors" "fmt" - "slices" "strings" "time" "github.com/google/uuid" - log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/server/activity" nbContext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/idp" @@ -20,6 +17,7 @@ import ( "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" + log "github.com/sirupsen/logrus" ) // createServiceUser creates a new service user under the given account. @@ -27,30 +25,29 @@ func (am *DefaultAccountManager) createServiceUser(ctx context.Context, accountI unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { - return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID) + return nil, err } - executingUser := account.Users[initiatorUserID] - if executingUser == nil { - return nil, status.Errorf(status.NotFound, "user not found") + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } - if !executingUser.HasAdminPower() { - return nil, status.Errorf(status.PermissionDenied, "only users with admin power can create service users") + + if !initiatorUser.HasAdminPower() { + return nil, status.NewAdminPermissionError() } if role == types.UserRoleOwner { - return nil, status.Errorf(status.InvalidArgument, "can't create a service user with owner role") + return nil, status.NewServiceUserRoleInvalidError() } newUserID := uuid.New().String() newUser := types.NewUser(newUserID, role, true, nonDeletable, serviceUserName, autoGroups, types.UserIssuedAPI) + newUser.AccountID = accountID log.WithContext(ctx).Debugf("New User: %v", newUser) - account.Users[newUserID] = newUser - err = am.Store.SaveAccount(ctx, account) - if err != nil { + if err = am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser); err != nil { return nil, err } @@ -87,40 +84,67 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u return nil, status.Errorf(status.PreconditionFailed, "IdP manager must be enabled to send user invites") } - if invite == nil { - return nil, fmt.Errorf("provided user update is nil") + if err := validateUserInvite(invite); err != nil { + return nil, err } - invitedRole := types.StrRoleToUserRole(invite.Role) - - switch { - case invite.Name == "": - return nil, status.Errorf(status.InvalidArgument, "name can't be empty") - case invite.Email == "": - return nil, status.Errorf(status.InvalidArgument, "email can't be empty") - case invitedRole == types.UserRoleOwner: - return nil, status.Errorf(status.InvalidArgument, "can't invite a user with owner role") - default: - } - - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { - return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID) + return nil, err } - initiatorUser, err := account.FindUser(userID) - if err != nil { - return nil, status.Errorf(status.NotFound, "initiator user with ID %s doesn't exist", userID) + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } inviterID := userID if initiatorUser.IsServiceUser { - inviterID = account.CreatedBy + createdBy, err := am.Store.GetAccountCreatedBy(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + inviterID = createdBy } + idpUser, err := am.createNewIdpUser(ctx, accountID, inviterID, invite) + if err != nil { + return nil, err + } + + newUser := &types.User{ + Id: idpUser.ID, + AccountID: accountID, + Role: types.StrRoleToUserRole(invite.Role), + AutoGroups: invite.AutoGroups, + Issued: invite.Issued, + IntegrationReference: invite.IntegrationReference, + CreatedAt: time.Now().UTC(), + } + + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + if err = am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser); err != nil { + return nil, err + } + + _, err = am.refreshCache(ctx, accountID) + if err != nil { + return nil, err + } + + am.StoreEvent(ctx, userID, newUser.Id, accountID, activity.UserInvited, nil) + + return newUser.ToUserInfo(idpUser, settings) +} + +// createNewIdpUser validates the invite and creates a new user in the IdP +func (am *DefaultAccountManager) createNewIdpUser(ctx context.Context, accountID string, inviterID string, invite *types.UserInfo) (*idp.UserData, error) { // inviterUser is the one who is inviting the new user - inviterUser, err := am.lookupUserInCache(ctx, inviterID, account) - if err != nil || inviterUser == nil { + inviterUser, err := am.lookupUserInCache(ctx, inviterID, accountID) + if err != nil { return nil, status.Errorf(status.NotFound, "inviter user with ID %s doesn't exist in IdP", inviterID) } @@ -143,34 +167,7 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u return nil, status.Errorf(status.UserAlreadyExists, "can't invite a user with an existing NetBird account") } - idpUser, err := am.idpManager.CreateUser(ctx, invite.Email, invite.Name, accountID, inviterUser.Email) - if err != nil { - return nil, err - } - - newUser := &types.User{ - Id: idpUser.ID, - Role: invitedRole, - AutoGroups: invite.AutoGroups, - Issued: invite.Issued, - IntegrationReference: invite.IntegrationReference, - CreatedAt: time.Now().UTC(), - } - account.Users[idpUser.ID] = newUser - - err = am.Store.SaveAccount(ctx, account) - if err != nil { - return nil, err - } - - _, err = am.refreshCache(ctx, account.Id) - if err != nil { - return nil, err - } - - am.StoreEvent(ctx, userID, newUser.Id, accountID, activity.UserInvited, nil) - - return newUser.ToUserInfo(idpUser, account.Settings) + return am.idpManager.CreateUser(ctx, invite.Email, invite.Name, accountID, inviterUser.Email) } func (am *DefaultAccountManager) GetUserByID(ctx context.Context, id string) (*types.User, error) { @@ -210,60 +207,51 @@ func (am *DefaultAccountManager) GetUser(ctx context.Context, claims jwtclaims.A // ListUsers returns lists of all users under the account. // It doesn't populate user information such as email or name. func (am *DefaultAccountManager) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) { - unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) - defer unlock() - - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return nil, err - } - - users := make([]*types.User, 0, len(account.Users)) - for _, item := range account.Users { - users = append(users, item) - } - - return users, nil + return am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) } -func (am *DefaultAccountManager) deleteServiceUser(ctx context.Context, account *types.Account, initiatorUserID string, targetUser *types.User) { +func (am *DefaultAccountManager) deleteServiceUser(ctx context.Context, accountID string, initiatorUserID string, targetUser *types.User) error { + if err := am.Store.DeleteUser(ctx, store.LockingStrengthUpdate, accountID, targetUser.Id); err != nil { + return err + } meta := map[string]any{"name": targetUser.ServiceUserName, "created_at": targetUser.CreatedAt} - am.StoreEvent(ctx, initiatorUserID, targetUser.Id, account.Id, activity.ServiceUserDeleted, meta) - delete(account.Users, targetUser.Id) + am.StoreEvent(ctx, initiatorUserID, targetUser.Id, accountID, activity.ServiceUserDeleted, meta) + return nil } // DeleteUser deletes a user from the given account. -func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error { +func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, initiatorUserID, targetUserID string) error { if initiatorUserID == targetUserID { return status.Errorf(status.InvalidArgument, "self deletion is not allowed") } + unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { return err } - executingUser := account.Users[initiatorUserID] - if executingUser == nil { - return status.Errorf(status.NotFound, "user not found") - } - if !executingUser.HasAdminPower() { - return status.Errorf(status.PermissionDenied, "only users with admin power can delete users") + if initiatorUser.AccountID != accountID { + return status.NewUserNotPartOfAccountError() } - targetUser := account.Users[targetUserID] - if targetUser == nil { - return status.Errorf(status.NotFound, "target user not found") + if !initiatorUser.HasAdminPower() { + return status.NewAdminPermissionError() + } + + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + if err != nil { + return err } if targetUser.Role == types.UserRoleOwner { - return status.Errorf(status.PermissionDenied, "unable to delete a user with owner role") + return status.NewOwnerDeletePermissionError() } // disable deleting integration user if the initiator is not admin service user - if targetUser.Issued == types.UserIssuedIntegration && !executingUser.IsServiceUser { + if targetUser.Issued == types.UserIssuedIntegration && !initiatorUser.IsServiceUser { return status.Errorf(status.PermissionDenied, "only integration service user can delete this user") } @@ -273,64 +261,26 @@ func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, init return status.Errorf(status.PermissionDenied, "service user is marked as non-deletable") } - am.deleteServiceUser(ctx, account, initiatorUserID, targetUser) - return am.Store.SaveAccount(ctx, account) + return am.deleteServiceUser(ctx, accountID, initiatorUserID, targetUser) } - return am.deleteRegularUser(ctx, account, initiatorUserID, targetUserID) -} - -func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, account *types.Account, initiatorUserID, targetUserID string) error { - meta, updateAccountPeers, err := am.prepareUserDeletion(ctx, account, initiatorUserID, targetUserID) + userInfo, err := am.getUserInfo(ctx, targetUser, accountID) if err != nil { return err } - delete(account.Users, targetUserID) - if updateAccountPeers { - account.Network.IncSerial() - } - - err = am.Store.SaveAccount(ctx, account) + updateAccountPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, userInfo) if err != nil { return err } - am.StoreEvent(ctx, initiatorUserID, targetUserID, account.Id, activity.UserDeleted, meta) if updateAccountPeers { - am.UpdateAccountPeers(ctx, account.Id) + am.UpdateAccountPeers(ctx, accountID) } return nil } -func (am *DefaultAccountManager) deleteUserPeers(ctx context.Context, initiatorUserID string, targetUserID string, account *types.Account) (bool, error) { - peers, err := account.FindUserPeers(targetUserID) - if err != nil { - return false, status.Errorf(status.Internal, "failed to find user peers") - } - - hadPeers := len(peers) > 0 - if !hadPeers { - return false, nil - } - - eventsToStore, err := deletePeers(ctx, am, am.Store, account.Id, initiatorUserID, peers) - if err != nil { - return false, err - } - - for _, storeEvent := range eventsToStore { - storeEvent() - } - - for _, peer := range peers { - account.DeletePeer(peer.ID) - } - - return hadPeers, nil -} - // InviteUser resend invitations to users who haven't activated their accounts prior to the expiration period. func (am *DefaultAccountManager) InviteUser(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error { unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) @@ -340,13 +290,17 @@ func (am *DefaultAccountManager) InviteUser(ctx context.Context, accountID strin return status.Errorf(status.PreconditionFailed, "IdP manager must be enabled to send user invites") } - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { - return status.Errorf(status.NotFound, "account %s doesn't exist", accountID) + return err + } + + if initiatorUser.AccountID != accountID { + return status.NewUserNotPartOfAccountError() } // check if the user is already registered with this ID - user, err := am.lookupUserInCache(ctx, targetUserID, account) + user, err := am.lookupUserInCache(ctx, targetUserID, accountID) if err != nil { return err } @@ -384,35 +338,31 @@ func (am *DefaultAccountManager) CreatePAT(ctx context.Context, accountID string return nil, status.Errorf(status.InvalidArgument, "expiration has to be between 1 and 365") } - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { return nil, err } - targetUser, ok := account.Users[targetUserID] - if !ok { - return nil, status.Errorf(status.NotFound, "user not found") + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } - executingUser, ok := account.Users[initiatorUserID] - if !ok { - return nil, status.Errorf(status.NotFound, "user not found") + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + if err != nil { + return nil, err } - if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) { - return nil, status.Errorf(status.PermissionDenied, "no permission to create PAT for this user") + if initiatorUserID != targetUserID && !(initiatorUser.HasAdminPower() && targetUser.IsServiceUser) { + return nil, status.NewAdminPermissionError() } - pat, err := types.CreateNewPAT(tokenName, expiresIn, executingUser.Id) + pat, err := types.CreateNewPAT(tokenName, expiresIn, targetUserID, initiatorUser.Id) if err != nil { return nil, status.Errorf(status.Internal, "failed to create PAT: %v", err) } - targetUser.PATs[pat.ID] = &pat.PersonalAccessToken - - err = am.Store.SaveAccount(ctx, account) - if err != nil { - return nil, status.Errorf(status.Internal, "failed to save account: %v", err) + if err = am.Store.SavePAT(ctx, store.LockingStrengthUpdate, &pat.PersonalAccessToken); err != nil { + return nil, err } meta := map[string]any{"name": pat.Name, "is_service_user": targetUser.IsServiceUser, "user_name": targetUser.ServiceUserName} @@ -426,48 +376,36 @@ func (am *DefaultAccountManager) DeletePAT(ctx context.Context, accountID string unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { - return status.Errorf(status.NotFound, "account not found: %s", err) + return err } - targetUser, ok := account.Users[targetUserID] - if !ok { - return status.Errorf(status.NotFound, "user not found") + if initiatorUser.AccountID != accountID { + return status.NewUserNotPartOfAccountError() } - executingUser, ok := account.Users[initiatorUserID] - if !ok { - return status.Errorf(status.NotFound, "user not found") + if initiatorUserID != targetUserID && initiatorUser.IsRegularUser() { + return status.NewAdminPermissionError() } - if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) { - return status.Errorf(status.PermissionDenied, "no permission to delete PAT for this user") - } - - pat := targetUser.PATs[tokenID] - if pat == nil { - return status.Errorf(status.NotFound, "PAT not found") - } - - err = am.Store.DeleteTokenID2UserIDIndex(pat.ID) + pat, err := am.Store.GetPATByID(ctx, store.LockingStrengthShare, targetUserID, tokenID) if err != nil { - return status.Errorf(status.Internal, "Failed to delete token id index: %s", err) + return err } - err = am.Store.DeleteHashedPAT2TokenIDIndex(pat.HashedToken) + + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) if err != nil { - return status.Errorf(status.Internal, "Failed to delete hashed token index: %s", err) + return err + } + + if err = am.Store.DeletePAT(ctx, store.LockingStrengthUpdate, targetUserID, tokenID); err != nil { + return err } meta := map[string]any{"name": pat.Name, "is_service_user": targetUser.IsServiceUser, "user_name": targetUser.ServiceUserName} am.StoreEvent(ctx, initiatorUserID, targetUserID, accountID, activity.PersonalAccessTokenDeleted, meta) - delete(targetUser.PATs, tokenID) - - err = am.Store.SaveAccount(ctx, account) - if err != nil { - return status.Errorf(status.Internal, "Failed to save account: %s", err) - } return nil } @@ -478,22 +416,15 @@ func (am *DefaultAccountManager) GetPAT(ctx context.Context, accountID string, i return nil, err } - targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) - if err != nil { - return nil, err + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } - if (initiatorUserID != targetUserID && !initiatorUser.IsAdminOrServiceUser()) || initiatorUser.AccountID != accountID { - return nil, status.Errorf(status.PermissionDenied, "no permission to get PAT for this user") + if initiatorUserID != targetUserID && initiatorUser.IsRegularUser() { + return nil, status.NewAdminPermissionError() } - for _, pat := range targetUser.PATsG { - if pat.ID == tokenID { - return pat.Copy(), nil - } - } - - return nil, status.Errorf(status.NotFound, "PAT not found") + return am.Store.GetPATByID(ctx, store.LockingStrengthShare, targetUserID, tokenID) } // GetAllPATs returns all PATs for a user @@ -503,21 +434,15 @@ func (am *DefaultAccountManager) GetAllPATs(ctx context.Context, accountID strin return nil, err } - targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) - if err != nil { - return nil, err + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } - if (initiatorUserID != targetUserID && !initiatorUser.IsAdminOrServiceUser()) || initiatorUser.AccountID != accountID { - return nil, status.Errorf(status.PermissionDenied, "no permission to get PAT for this user") + if initiatorUserID != targetUserID && initiatorUser.IsRegularUser() { + return nil, status.NewAdminPermissionError() } - pats := make([]*types.PersonalAccessToken, 0, len(targetUser.PATsG)) - for _, pat := range targetUser.PATsG { - pats = append(pats, pat.Copy()) - } - - return pats, nil + return am.Store.GetUserPATs(ctx, store.LockingStrengthShare, targetUserID) } // SaveUser saves updates to the given user. If the user doesn't exist, it will throw status.NotFound error. @@ -528,10 +453,6 @@ func (am *DefaultAccountManager) SaveUser(ctx context.Context, accountID, initia // SaveOrAddUser updates the given user. If addIfNotExists is set to true it will add user when no exist // Only User.AutoGroups, User.Role, and User.Blocked fields are allowed to be updated for now. func (am *DefaultAccountManager) SaveOrAddUser(ctx context.Context, accountID, initiatorUserID string, update *types.User, addIfNotExists bool) (*types.UserInfo, error) { - if update == nil { - return nil, status.Errorf(status.InvalidArgument, "provided user update is nil") - } - unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() @@ -555,125 +476,113 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, return nil, nil //nolint:nilnil } - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { return nil, err } - initiatorUser, err := account.FindUser(initiatorUserID) - if err != nil { - return nil, err + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } if !initiatorUser.HasAdminPower() || initiatorUser.IsBlocked() { - return nil, status.Errorf(status.PermissionDenied, "only users with admin power are authorized to perform user update operations") + return nil, status.NewAdminPermissionError() } - updatedUsers := make([]*types.UserInfo, 0, len(updates)) - var ( - expiredPeers []*nbpeer.Peer - userIDs []string - eventsToStore []func() - ) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } - for _, update := range updates { - if update == nil { - return nil, status.Errorf(status.InvalidArgument, "provided user update is nil") - } + var updateAccountPeers bool + var peersToExpire []*nbpeer.Peer + var addUserEvents []func() + var usersToSave = make([]*types.User, 0, len(updates)) - userIDs = append(userIDs, update.Id) + groups, err := am.Store.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, fmt.Errorf("error getting account groups: %w", err) + } - oldUser := account.Users[update.Id] - if oldUser == nil { - if !addIfNotExists { - return nil, status.Errorf(status.NotFound, "user to update doesn't exist: %s", update.Id) + groupsMap := make(map[string]*types.Group, len(groups)) + for _, group := range groups { + groupsMap[group.ID] = group + } + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + for _, update := range updates { + if update == nil { + return status.Errorf(status.InvalidArgument, "provided user update is nil") } - // when addIfNotExists is set to true, the newUser will use all fields from the update input - oldUser = update - } - if err := validateUserUpdate(account, initiatorUser, oldUser, update); err != nil { - return nil, err - } - - // only auto groups, revoked status, and integration reference can be updated for now - newUser := oldUser.Copy() - newUser.Role = update.Role - newUser.Blocked = update.Blocked - newUser.AutoGroups = update.AutoGroups - // these two fields can't be set via API, only via direct call to the method - newUser.Issued = update.Issued - newUser.IntegrationReference = update.IntegrationReference - - transferredOwnerRole := handleOwnerRoleTransfer(account, initiatorUser, update) - account.Users[newUser.Id] = newUser - - if !oldUser.IsBlocked() && update.IsBlocked() { - // expire peers that belong to the user who's getting blocked - blockedPeers, err := account.FindUserPeers(update.Id) + userHadPeers, updatedUser, userPeersToExpire, userEvents, err := am.processUserUpdate( + ctx, transaction, groupsMap, initiatorUser, update, addIfNotExists, settings, + ) if err != nil { - return nil, err + return fmt.Errorf("failed to process user update: %w", err) + } + usersToSave = append(usersToSave, updatedUser) + addUserEvents = append(addUserEvents, userEvents...) + peersToExpire = append(peersToExpire, userPeersToExpire...) + + if userHadPeers { + updateAccountPeers = true } - expiredPeers = append(expiredPeers, blockedPeers...) } - - peerGroupsAdded := make(map[string][]string) - peerGroupsRemoved := make(map[string][]string) - if update.AutoGroups != nil && account.Settings.GroupsPropagationEnabled { - removedGroups := util.Difference(oldUser.AutoGroups, update.AutoGroups) - // need force update all auto groups in any case they will not be duplicated - peerGroupsAdded = account.UserGroupsAddToPeers(oldUser.Id, update.AutoGroups...) - peerGroupsRemoved = account.UserGroupsRemoveFromPeers(oldUser.Id, removedGroups...) - } - - userUpdateEvents := am.prepareUserUpdateEvents(ctx, initiatorUser.Id, oldUser, newUser, account, transferredOwnerRole) - eventsToStore = append(eventsToStore, userUpdateEvents...) - - userGroupsEvents := am.prepareUserGroupsEvents(ctx, initiatorUser.Id, oldUser, newUser, account, peerGroupsAdded, peerGroupsRemoved) - eventsToStore = append(eventsToStore, userGroupsEvents...) - - updatedUserInfo, err := getUserInfo(ctx, am, newUser, account) - if err != nil { - return nil, err - } - updatedUsers = append(updatedUsers, updatedUserInfo) + return transaction.SaveUsers(ctx, store.LockingStrengthUpdate, usersToSave) + }) + if err != nil { + return nil, err } - if len(expiredPeers) > 0 { - if err := am.expireAndUpdatePeers(ctx, account.Id, expiredPeers); err != nil { + var updatedUsersInfo = make([]*types.UserInfo, 0, len(updates)) + + userInfos, err := am.GetUsersFromAccount(ctx, accountID, initiatorUserID) + if err != nil { + return nil, err + } + + for _, updatedUser := range usersToSave { + updatedUserInfo, ok := userInfos[updatedUser.Id] + if !ok || updatedUserInfo == nil { + return nil, fmt.Errorf("failed to get user: %s updated user info", updatedUser.Id) + } + updatedUsersInfo = append(updatedUsersInfo, updatedUserInfo) + } + + for _, addUserEvent := range addUserEvents { + addUserEvent() + } + + if len(peersToExpire) > 0 { + if err := am.expireAndUpdatePeers(ctx, accountID, peersToExpire); err != nil { log.WithContext(ctx).Errorf("failed update expired peers: %s", err) return nil, err } } - account.Network.IncSerial() - if err = am.Store.SaveAccount(ctx, account); err != nil { - return nil, err + if settings.GroupsPropagationEnabled && updateAccountPeers { + if err = am.Store.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return nil, fmt.Errorf("failed to increment network serial: %w", err) + } + am.UpdateAccountPeers(ctx, accountID) } - if account.Settings.GroupsPropagationEnabled && areUsersLinkedToPeers(account, userIDs) { - am.UpdateAccountPeers(ctx, account.Id) - } - - for _, storeEvent := range eventsToStore { - storeEvent() - } - - return updatedUsers, nil + return updatedUsersInfo, nil } // prepareUserUpdateEvents prepares a list user update events based on the changes between the old and new user data. -func (am *DefaultAccountManager) prepareUserUpdateEvents(ctx context.Context, initiatorUserID string, oldUser, newUser *types.User, account *types.Account, transferredOwnerRole bool) []func() { +func (am *DefaultAccountManager) prepareUserUpdateEvents(ctx context.Context, accountID string, initiatorUserID string, oldUser, newUser *types.User, transferredOwnerRole bool) []func() { var eventsToStore []func() if oldUser.IsBlocked() != newUser.IsBlocked() { if newUser.IsBlocked() { eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.UserBlocked, nil) + am.StoreEvent(ctx, initiatorUserID, oldUser.Id, accountID, activity.UserBlocked, nil) }) } else { eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.UserUnblocked, nil) + am.StoreEvent(ctx, initiatorUserID, oldUser.Id, accountID, activity.UserUnblocked, nil) }) } } @@ -681,115 +590,126 @@ func (am *DefaultAccountManager) prepareUserUpdateEvents(ctx context.Context, in switch { case transferredOwnerRole: eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.TransferredOwnerRole, nil) + am.StoreEvent(ctx, initiatorUserID, oldUser.Id, accountID, activity.TransferredOwnerRole, nil) }) case oldUser.Role != newUser.Role: eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.UserRoleUpdated, map[string]any{"role": newUser.Role}) + am.StoreEvent(ctx, initiatorUserID, oldUser.Id, accountID, activity.UserRoleUpdated, map[string]any{"role": newUser.Role}) }) } return eventsToStore } -func (am *DefaultAccountManager) prepareUserGroupsEvents(ctx context.Context, initiatorUserID string, oldUser, newUser *types.User, account *types.Account, peerGroupsAdded, peerGroupsRemoved map[string][]string) []func() { - var eventsToStore []func() - if newUser.AutoGroups != nil { - removedGroups := util.Difference(oldUser.AutoGroups, newUser.AutoGroups) - addedGroups := util.Difference(newUser.AutoGroups, oldUser.AutoGroups) +func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transaction store.Store, groupsMap map[string]*types.Group, + initiatorUser, update *types.User, addIfNotExists bool, settings *types.Settings) (bool, *types.User, []*nbpeer.Peer, []func(), error) { - removedEvents := am.handleGroupRemovedFromUser(ctx, initiatorUserID, oldUser, newUser, account, removedGroups, peerGroupsRemoved) - eventsToStore = append(eventsToStore, removedEvents...) - - addedEvents := am.handleGroupAddedToUser(ctx, initiatorUserID, oldUser, newUser, account, addedGroups, peerGroupsAdded) - eventsToStore = append(eventsToStore, addedEvents...) + if update == nil { + return false, nil, nil, nil, status.Errorf(status.InvalidArgument, "provided user update is nil") } - return eventsToStore + + oldUser, err := getUserOrCreateIfNotExists(ctx, transaction, update, addIfNotExists) + if err != nil { + return false, nil, nil, nil, err + } + + if err := validateUserUpdate(groupsMap, initiatorUser, oldUser, update); err != nil { + return false, nil, nil, nil, err + } + + // only auto groups, revoked status, and integration reference can be updated for now + updatedUser := oldUser.Copy() + updatedUser.AccountID = initiatorUser.AccountID + updatedUser.Role = update.Role + updatedUser.Blocked = update.Blocked + updatedUser.AutoGroups = update.AutoGroups + // these two fields can't be set via API, only via direct call to the method + updatedUser.Issued = update.Issued + updatedUser.IntegrationReference = update.IntegrationReference + + transferredOwnerRole, err := handleOwnerRoleTransfer(ctx, transaction, initiatorUser, update) + if err != nil { + return false, nil, nil, nil, err + } + + userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthUpdate, updatedUser.AccountID, update.Id) + if err != nil { + return false, nil, nil, nil, err + } + + var peersToExpire []*nbpeer.Peer + + if !oldUser.IsBlocked() && update.IsBlocked() { + peersToExpire = userPeers + } + + if update.AutoGroups != nil && settings.GroupsPropagationEnabled { + removedGroups := util.Difference(oldUser.AutoGroups, update.AutoGroups) + updatedGroups, err := updateUserPeersInGroups(groupsMap, userPeers, update.AutoGroups, removedGroups) + if err != nil { + return false, nil, nil, nil, fmt.Errorf("error modifying user peers in groups: %w", err) + } + + if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, updatedGroups); err != nil { + return false, nil, nil, nil, fmt.Errorf("error saving groups: %w", err) + } + } + + updateAccountPeers := len(userPeers) > 0 + userEventsToAdd := am.prepareUserUpdateEvents(ctx, updatedUser.AccountID, initiatorUser.Id, oldUser, updatedUser, transferredOwnerRole) + + return updateAccountPeers, updatedUser, peersToExpire, userEventsToAdd, nil } -func (am *DefaultAccountManager) handleGroupAddedToUser(ctx context.Context, initiatorUserID string, oldUser, newUser *types.User, account *types.Account, addedGroups []string, peerGroupsAdded map[string][]string) []func() { - var eventsToStore []func() - for _, g := range addedGroups { - group := account.GetGroup(g) - if group != nil { - eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.GroupAddedToUser, - map[string]any{"group": group.Name, "group_id": group.ID, "is_service_user": newUser.IsServiceUser, "user_name": newUser.ServiceUserName}) - }) +// getUserOrCreateIfNotExists retrieves the existing user or creates a new one if it doesn't exist. +func getUserOrCreateIfNotExists(ctx context.Context, transaction store.Store, update *types.User, addIfNotExists bool) (*types.User, error) { + existingUser, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, update.Id) + if err != nil { + if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound { + if !addIfNotExists { + return nil, status.Errorf(status.NotFound, "user to update doesn't exist: %s", update.Id) + } + return update, nil // use all fields from update if addIfNotExists is true } + return nil, err } - for groupID, peerIDs := range peerGroupsAdded { - group := account.GetGroup(groupID) - for _, peerID := range peerIDs { - peer := account.GetPeer(peerID) - eventsToStore = append(eventsToStore, func() { - meta := map[string]any{ - "group": group.Name, "group_id": group.ID, - "peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(am.GetDNSDomain()), - } - am.StoreEvent(ctx, activity.SystemInitiator, peer.ID, account.Id, activity.GroupAddedToPeer, meta) - }) - } - } - return eventsToStore + return existingUser, nil } -func (am *DefaultAccountManager) handleGroupRemovedFromUser(ctx context.Context, initiatorUserID string, oldUser, newUser *types.User, account *types.Account, removedGroups []string, peerGroupsRemoved map[string][]string) []func() { - var eventsToStore []func() - for _, g := range removedGroups { - group := account.GetGroup(g) - if group != nil { - eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.GroupRemovedFromUser, - map[string]any{"group": group.Name, "group_id": group.ID, "is_service_user": newUser.IsServiceUser, "user_name": newUser.ServiceUserName}) - }) - - } else { - log.WithContext(ctx).Errorf("group %s not found while saving user activity event of account %s", g, account.Id) - } - } - for groupID, peerIDs := range peerGroupsRemoved { - group := account.GetGroup(groupID) - for _, peerID := range peerIDs { - peer := account.GetPeer(peerID) - eventsToStore = append(eventsToStore, func() { - meta := map[string]any{ - "group": group.Name, "group_id": group.ID, - "peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(am.GetDNSDomain()), - } - am.StoreEvent(ctx, activity.SystemInitiator, peer.ID, account.Id, activity.GroupRemovedFromPeer, meta) - }) - } - } - return eventsToStore -} - -func handleOwnerRoleTransfer(account *types.Account, initiatorUser, update *types.User) bool { +func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initiatorUser, update *types.User) (bool, error) { if initiatorUser.Role == types.UserRoleOwner && initiatorUser.Id != update.Id && update.Role == types.UserRoleOwner { newInitiatorUser := initiatorUser.Copy() newInitiatorUser.Role = types.UserRoleAdmin - account.Users[initiatorUser.Id] = newInitiatorUser - return true + + if err := transaction.SaveUser(ctx, store.LockingStrengthUpdate, newInitiatorUser); err != nil { + return false, err + } + return true, nil } - return false + return false, nil } // getUserInfo retrieves the UserInfo for a given User and Account. // If the AccountManager has a non-nil idpManager and the User is not a service user, // it will attempt to look up the UserData from the cache. -func getUserInfo(ctx context.Context, am *DefaultAccountManager, user *types.User, account *types.Account) (*types.UserInfo, error) { +func (am *DefaultAccountManager) getUserInfo(ctx context.Context, user *types.User, accountID string) (*types.UserInfo, error) { + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + if !isNil(am.idpManager) && !user.IsServiceUser { - userData, err := am.lookupUserInCache(ctx, user.Id, account) + userData, err := am.lookupUserInCache(ctx, user.Id, accountID) if err != nil { return nil, err } - return user.ToUserInfo(userData, account.Settings) + return user.ToUserInfo(userData, settings) } - return user.ToUserInfo(nil, account.Settings) + return user.ToUserInfo(nil, settings) } // validateUserUpdate validates the update operation for a user. -func validateUserUpdate(account *types.Account, initiatorUser, oldUser, update *types.User) error { +func validateUserUpdate(groupsMap map[string]*types.Group, initiatorUser, oldUser, update *types.User) error { if initiatorUser.HasAdminPower() && initiatorUser.Id == update.Id && oldUser.Blocked != update.Blocked { return status.Errorf(status.PermissionDenied, "admins can't block or unblock themselves") } @@ -810,12 +730,12 @@ func validateUserUpdate(account *types.Account, initiatorUser, oldUser, update * } for _, newGroupID := range update.AutoGroups { - group, ok := account.Groups[newGroupID] + group, ok := groupsMap[newGroupID] if !ok { return status.Errorf(status.InvalidArgument, "provided group ID %s in the user %s update doesn't exist", newGroupID, update.Id) } - if group.Name == "All" { + if group.IsGroupAll() { return status.Errorf(status.InvalidArgument, "can't add All group to the user") } } @@ -864,22 +784,38 @@ func (am *DefaultAccountManager) GetOrCreateAccountByUser(ctx context.Context, u // GetUsersFromAccount performs a batched request for users from IDP by account ID apply filter on what data to return // based on provided user role. -func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error) { - account, err := am.Store.GetAccount(ctx, accountID) +func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accountID, initiatorUserID string) (map[string]*types.UserInfo, error) { + accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) if err != nil { return nil, err } - user, err := account.FindUser(userID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + if err != nil { + return nil, err + } + + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() + } + + return am.BuildUserInfosForAccount(ctx, accountID, initiatorUserID, accountUsers) +} + +// BuildUserInfosForAccount builds user info for the given account. +func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) { + var queriedUsers []*idp.UserData + var err error + + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { return nil, err } - queriedUsers := make([]*idp.UserData, 0) if !isNil(am.idpManager) { - users := make(map[string]userLoggedInOnce, len(account.Users)) + users := make(map[string]userLoggedInOnce, len(accountUsers)) usersFromIntegration := make([]*idp.UserData, 0) - for _, user := range account.Users { + for _, user := range accountUsers { if user.Issued == types.UserIssuedIntegration { key := user.IntegrationReference.CacheKey(accountID, user.Id) info, err := am.externalCacheManager.Get(am.ctx, key) @@ -904,33 +840,40 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun queriedUsers = append(queriedUsers, usersFromIntegration...) } - userInfos := make([]*types.UserInfo, 0) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + userInfosMap := make(map[string]*types.UserInfo) // in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo if len(queriedUsers) == 0 { - for _, accountUser := range account.Users { - if !(user.HasAdminPower() || user.IsServiceUser || user.Id == accountUser.Id) { + for _, accountUser := range accountUsers { + if initiatorUser.IsRegularUser() && initiatorUser.Id != accountUser.Id { // if user is not an admin then show only current user and do not show other users continue } - info, err := accountUser.ToUserInfo(nil, account.Settings) + + info, err := accountUser.ToUserInfo(nil, settings) if err != nil { return nil, err } - userInfos = append(userInfos, info) + userInfosMap[accountUser.Id] = info } - return userInfos, nil + + return userInfosMap, nil } - for _, localUser := range account.Users { - if !(user.HasAdminPower() || user.IsServiceUser) && user.Id != localUser.Id { + for _, localUser := range accountUsers { + if initiatorUser.IsRegularUser() && initiatorUser.Id != localUser.Id { // if user is not an admin then show only current user and do not show other users continue } var info *types.UserInfo if queriedUser, contains := findUserInIDPUserdata(localUser.Id, queriedUsers); contains { - info, err = localUser.ToUserInfo(queriedUser, account.Settings) + info, err = localUser.ToUserInfo(queriedUser, settings) if err != nil { return nil, err } @@ -943,7 +886,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun dashboardViewPermissions := "full" if !localUser.HasAdminPower() { dashboardViewPermissions = "limited" - if account.Settings.RegularUsersViewBlocked { + if settings.RegularUsersViewBlocked { dashboardViewPermissions = "blocked" } } @@ -960,10 +903,10 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun Permissions: types.UserPermissions{DashboardView: dashboardViewPermissions}, } } - userInfos = append(userInfos, info) + userInfosMap[info.ID] = info } - return userInfos, nil + return userInfosMap, nil } // expireAndUpdatePeers expires all peers of the given user and updates them in the account @@ -1017,55 +960,34 @@ func (am *DefaultAccountManager) deleteUserFromIDP(ctx context.Context, targetUs return nil } -func (am *DefaultAccountManager) getEmailAndNameOfTargetUser(ctx context.Context, accountId, initiatorId, targetId string) (string, string, error) { - userInfos, err := am.GetUsersFromAccount(ctx, accountId, initiatorId) - if err != nil { - return "", "", err - } - for _, ui := range userInfos { - if ui.ID == targetId { - return ui.Email, ui.Name, nil - } - } - - return "", "", fmt.Errorf("user info not found for user: %s", targetId) -} - // DeleteRegularUsers deletes regular users from an account. // Note: This function does not acquire the global lock. // It is the caller's responsibility to ensure proper locking is in place before invoking this method. // // If an error occurs while deleting the user, the function skips it and continues deleting other users. // Errors are collected and returned at the end. -func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error { - account, err := am.Store.GetAccount(ctx, accountID) +func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error { + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { return err } - executingUser := account.Users[initiatorUserID] - if executingUser == nil { - return status.Errorf(status.NotFound, "user not found") - } - if !executingUser.HasAdminPower() { - return status.Errorf(status.PermissionDenied, "only users with admin power can delete users") + if !initiatorUser.HasAdminPower() { + return status.NewAdminPermissionError() } - var ( - allErrors error - updateAccountPeers bool - ) + var allErrors error + var updateAccountPeers bool - deletedUsersMeta := make(map[string]map[string]any) for _, targetUserID := range targetUserIDs { if initiatorUserID == targetUserID { allErrors = errors.Join(allErrors, errors.New("self deletion is not allowed")) continue } - targetUser := account.Users[targetUserID] - if targetUser == nil { - allErrors = errors.Join(allErrors, fmt.Errorf("target user: %s not found", targetUserID)) + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + if err != nil { + allErrors = errors.Join(allErrors, err) continue } @@ -1075,88 +997,97 @@ func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, account } // disable deleting integration user if the initiator is not admin service user - if targetUser.Issued == types.UserIssuedIntegration && !executingUser.IsServiceUser { + if targetUser.Issued == types.UserIssuedIntegration && !initiatorUser.IsServiceUser { allErrors = errors.Join(allErrors, errors.New("only integration service user can delete this user")) continue } - meta, hadPeers, err := am.prepareUserDeletion(ctx, account, initiatorUserID, targetUserID) - if err != nil { - allErrors = errors.Join(allErrors, fmt.Errorf("failed to delete user %s: %s", targetUserID, err)) + userInfo, ok := userInfos[targetUserID] + if !ok || userInfo == nil { + allErrors = errors.Join(allErrors, fmt.Errorf("user info not found for user: %s", targetUserID)) continue } - if hadPeers { - updateAccountPeers = true + userHadPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, userInfo) + if err != nil { + allErrors = errors.Join(allErrors, err) + continue } - delete(account.Users, targetUserID) - deletedUsersMeta[targetUserID] = meta - } - - if updateAccountPeers { - account.Network.IncSerial() - } - err = am.Store.SaveAccount(ctx, account) - if err != nil { - return fmt.Errorf("failed to delete users: %w", err) + if userHadPeers { + updateAccountPeers = true + } } if updateAccountPeers { am.UpdateAccountPeers(ctx, accountID) } - for targetUserID, meta := range deletedUsersMeta { - am.StoreEvent(ctx, initiatorUserID, targetUserID, account.Id, activity.UserDeleted, meta) - } - return allErrors } -func (am *DefaultAccountManager) prepareUserDeletion(ctx context.Context, account *types.Account, initiatorUserID, targetUserID string) (map[string]any, bool, error) { - tuEmail, tuName, err := am.getEmailAndNameOfTargetUser(ctx, account.Id, initiatorUserID, targetUserID) - if err != nil { - log.WithContext(ctx).Errorf("failed to resolve email address: %s", err) - return nil, false, err - } - +// deleteRegularUser deletes a specified user and their related peers from the account. +func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountID, initiatorUserID string, targetUserInfo *types.UserInfo) (bool, error) { if !isNil(am.idpManager) { // Delete if the user already exists in the IdP. Necessary in cases where a user account // was created where a user account was provisioned but the user did not sign in - _, err = am.idpManager.GetUserDataByID(ctx, targetUserID, idp.AppMetadata{WTAccountID: account.Id}) + _, err := am.idpManager.GetUserDataByID(ctx, targetUserInfo.ID, idp.AppMetadata{WTAccountID: accountID}) if err == nil { - err = am.deleteUserFromIDP(ctx, targetUserID, account.Id) + err = am.deleteUserFromIDP(ctx, targetUserInfo.ID, accountID) if err != nil { - log.WithContext(ctx).Debugf("failed to delete user from IDP: %s", targetUserID) - return nil, false, err + log.WithContext(ctx).Debugf("failed to delete user from IDP: %s", targetUserInfo.ID) + return false, err } } else { - log.WithContext(ctx).Debugf("skipped deleting user %s from IDP, error: %v", targetUserID, err) + log.WithContext(ctx).Debugf("skipped deleting user %s from IDP, error: %v", targetUserInfo.ID, err) } } - hadPeers, err := am.deleteUserPeers(ctx, initiatorUserID, targetUserID, account) + var addPeerRemovedEvents []func() + var updateAccountPeers bool + var targetUser *types.User + var err error + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + targetUser, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserInfo.ID) + if err != nil { + return fmt.Errorf("failed to get user to delete: %w", err) + } + + userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, targetUserInfo.ID) + if err != nil { + return fmt.Errorf("failed to get user peers: %w", err) + } + + if len(userPeers) > 0 { + updateAccountPeers = true + addPeerRemovedEvents, err = deletePeers(ctx, am, transaction, accountID, targetUserInfo.ID, userPeers) + if err != nil { + return fmt.Errorf("failed to delete user peers: %w", err) + } + } + + if err = transaction.DeleteUser(ctx, store.LockingStrengthUpdate, accountID, targetUserInfo.ID); err != nil { + return fmt.Errorf("failed to delete user: %s %w", targetUserInfo.ID, err) + } + + return nil + }) if err != nil { - return nil, false, err + return false, err } - u, err := account.FindUser(targetUserID) - if err != nil { - log.WithContext(ctx).Errorf("failed to find user %s for deletion, this should never happen: %s", targetUserID, err) + for _, addPeerRemovedEvent := range addPeerRemovedEvents { + addPeerRemovedEvent() } + meta := map[string]any{"name": targetUserInfo.Name, "email": targetUserInfo.Email, "created_at": targetUser.CreatedAt} + am.StoreEvent(ctx, initiatorUserID, targetUser.Id, accountID, activity.UserDeleted, meta) - var tuCreatedAt time.Time - if u != nil { - tuCreatedAt = u.CreatedAt - } - - return map[string]any{"name": tuName, "email": tuEmail, "created_at": tuCreatedAt}, hadPeers, nil + return updateAccountPeers, nil } // updateUserPeersInGroups updates the user's peers in the specified groups by adding or removing them. -func (am *DefaultAccountManager) updateUserPeersInGroups(accountGroups map[string]*types.Group, peers []*nbpeer.Peer, groupsToAdd, - groupsToRemove []string) (groupsToUpdate []*types.Group, err error) { - +func updateUserPeersInGroups(accountGroups map[string]*types.Group, peers []*nbpeer.Peer, groupsToAdd, groupsToRemove []string) (groupsToUpdate []*types.Group, err error) { if len(groupsToAdd) == 0 && len(groupsToRemove) == 0 { return } @@ -1230,12 +1161,22 @@ func findUserInIDPUserdata(userID string, userData []*idp.UserData) (*idp.UserDa return nil, false } -// areUsersLinkedToPeers checks if any of the given userIDs are linked to any of the peers in the account. -func areUsersLinkedToPeers(account *types.Account, userIDs []string) bool { - for _, peer := range account.Peers { - if slices.Contains(userIDs, peer.UserID) { - return true - } +func validateUserInvite(invite *types.UserInfo) error { + if invite == nil { + return fmt.Errorf("provided user update is nil") } - return false + + invitedRole := types.StrRoleToUserRole(invite.Role) + + switch { + case invite.Name == "": + return status.Errorf(status.InvalidArgument, "name can't be empty") + case invite.Email == "": + return status.Errorf(status.InvalidArgument, "email can't be empty") + case invitedRole == types.UserRoleOwner: + return status.Errorf(status.InvalidArgument, "can't invite a user with owner role") + default: + } + + return nil } diff --git a/management/server/user_test.go b/management/server/user_test.go index a028d164b..4a532c8a6 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -11,6 +11,7 @@ import ( cacheStore "github.com/eko/gocache/v3/store" "github.com/google/go-cmp/cmp" "github.com/netbirdio/netbird/management/server/util" + "golang.org/x/exp/maps" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" @@ -45,7 +46,7 @@ const ( ) func TestUser_CreatePAT_ForSameUser(t *testing.T) { - store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) if err != nil { t.Fatalf("Error when creating store: %s", err) } @@ -53,13 +54,13 @@ func TestUser_CreatePAT_ForSameUser(t *testing.T) { account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") - err = store.SaveAccount(context.Background(), account) + err = s.SaveAccount(context.Background(), account) if err != nil { t.Fatalf("Error when saving account: %s", err) } am := DefaultAccountManager{ - Store: store, + Store: s, eventStore: &activity.InMemoryEventStore{}, } @@ -81,7 +82,7 @@ func TestUser_CreatePAT_ForSameUser(t *testing.T) { assert.Equal(t, pat.ID, tokenID) - user, err := am.Store.GetUserByTokenID(context.Background(), tokenID) + user, err := am.Store.GetUserByPATID(context.Background(), store.LockingStrengthShare, tokenID) if err != nil { t.Fatalf("Error when getting user by token ID: %s", err) } @@ -855,7 +856,7 @@ func TestUser_DeleteUser_RegularUsers(t *testing.T) { { name: "Delete non-existent user", userIDs: []string{"non-existent-user"}, - expectedReasons: []string{"target user: non-existent-user not found"}, + expectedReasons: []string{"user: non-existent-user not found"}, expectedNotDeleted: []string{}, }, { @@ -867,7 +868,10 @@ func TestUser_DeleteUser_RegularUsers(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err = am.DeleteRegularUsers(context.Background(), mockAccountID, mockUserID, tc.userIDs) + userInfos, err := am.BuildUserInfosForAccount(context.Background(), mockAccountID, mockUserID, maps.Values(account.Users)) + assert.NoError(t, err) + + err = am.DeleteRegularUsers(context.Background(), mockAccountID, mockUserID, tc.userIDs, userInfos) if len(tc.expectedReasons) > 0 { assert.Error(t, err) var foundExpectedErrors int From 8fb5a9ce1145cd53303de5bf1e33041a5d7de0ba Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Tue, 18 Feb 2025 02:08:03 +0300 Subject: [PATCH 03/23] [management] add batching support for SaveUsers and SaveGroups (#3341) Signed-off-by: bcmmbaga --- management/server/store/sql_store.go | 4 +- management/server/store/sql_store_test.go | 74 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 6a6753595..5c4ddf666 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -420,7 +420,7 @@ func (s *SqlStore) SaveUsers(ctx context.Context, lockStrength LockingStrength, return nil } - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&users) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&users) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save users to store: %s", result.Error) return status.Errorf(status.Internal, "failed to save users to store") @@ -444,7 +444,7 @@ func (s *SqlStore) SaveGroups(ctx context.Context, lockStrength LockingStrength, return nil } - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&groups) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&groups) if result.Error != nil { return status.Errorf(status.Internal, "failed to save groups to store: %v", result.Error) } diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 4dcdadf44..6e04c7d9d 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -1331,6 +1331,14 @@ func TestSqlStore_SaveGroups(t *testing.T) { } err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups) require.NoError(t, err) + + groups[1].Peers = []string{} + err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups) + require.NoError(t, err) + + group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groups[1].ID) + require.NoError(t, err) + require.Equal(t, groups[1], group) } func TestSqlStore_DeleteGroup(t *testing.T) { @@ -3046,6 +3054,14 @@ func TestSqlStore_SaveUsers(t *testing.T) { accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) require.NoError(t, err) require.Len(t, accountUsers, 4) + + users[1].AutoGroups = []string{"groupA", "groupC"} + err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users) + require.NoError(t, err) + + user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, users[1].Id) + require.NoError(t, err) + require.Equal(t, users[1].AutoGroups, user.AutoGroups) } func TestSqlStore_DeleteUser(t *testing.T) { @@ -3198,3 +3214,61 @@ func TestSqlStore_DeletePAT(t *testing.T) { require.Error(t, err) require.Nil(t, pat) } + +func TestSqlStore_SaveUsers_LargeBatch(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountUsers, 2) + + usersToSave := make([]*types.User, 0) + + for i := 1; i <= 8000; i++ { + usersToSave = append(usersToSave, &types.User{ + Id: fmt.Sprintf("user-%d", i), + AccountID: accountID, + Role: types.UserRoleUser, + }) + } + + err = store.SaveUsers(context.Background(), LockingStrengthUpdate, usersToSave) + require.NoError(t, err) + + accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Equal(t, 8002, len(accountUsers)) +} + +func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + accountGroups, err := store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountGroups, 3) + + groupsToSave := make([]*types.Group, 0) + + for i := 1; i <= 8000; i++ { + groupsToSave = append(groupsToSave, &types.Group{ + ID: fmt.Sprintf("%d", i), + AccountID: accountID, + Name: fmt.Sprintf("group-%d", i), + }) + } + + err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groupsToSave) + require.NoError(t, err) + + accountGroups, err = store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Equal(t, 8003, len(accountGroups)) +} From f67e56d3b9f018481470cc9ea42af86cfce81df4 Mon Sep 17 00:00:00 2001 From: Karsa Date: Tue, 18 Feb 2025 02:21:44 +0100 Subject: [PATCH 04/23] [client][ui] added accessible tray icons (#3335) Added accessible tray icons with: - dark mode support on Windows and Linux, kudos to @burgosz for the PoC - template icon support on MacOS Also added appropriate connecting status icons --- .goreleaser_ui.yaml | 4 +- client/ui/client_ui.go | 178 +++++++++++++++--- .../ui/netbird-systemtray-connected-dark.ico | Bin 0 -> 105144 bytes .../ui/netbird-systemtray-connected-dark.png | Bin 0 -> 5272 bytes .../ui/netbird-systemtray-connected-macos.png | Bin 0 -> 3858 bytes client/ui/netbird-systemtray-connected.ico | Bin 5139 -> 105151 bytes client/ui/netbird-systemtray-connected.png | Bin 9105 -> 5287 bytes .../ui/netbird-systemtray-connecting-dark.ico | Bin 0 -> 105128 bytes .../ui/netbird-systemtray-connecting-dark.png | Bin 0 -> 5434 bytes .../netbird-systemtray-connecting-macos.png | Bin 0 -> 3843 bytes client/ui/netbird-systemtray-connecting.ico | Bin 0 -> 105091 bytes client/ui/netbird-systemtray-connecting.png | Bin 0 -> 5412 bytes .../netbird-systemtray-disconnected-macos.png | Bin 0 -> 3491 bytes client/ui/netbird-systemtray-disconnected.ico | Bin 5167 -> 104575 bytes client/ui/netbird-systemtray-disconnected.png | Bin 9816 -> 4800 bytes client/ui/netbird-systemtray-error-dark.ico | Bin 0 -> 105062 bytes client/ui/netbird-systemtray-error-dark.png | Bin 0 -> 5279 bytes client/ui/netbird-systemtray-error-macos.png | Bin 0 -> 3837 bytes client/ui/netbird-systemtray-error.ico | Bin 0 -> 105013 bytes client/ui/netbird-systemtray-error.png | Bin 0 -> 5260 bytes client/ui/netbird-systemtray-update-cloud.ico | Bin 3647 -> 0 bytes client/ui/netbird-systemtray-update-cloud.png | Bin 5652 -> 0 bytes ...tbird-systemtray-update-connected-dark.ico | Bin 0 -> 104704 bytes ...tbird-systemtray-update-connected-dark.png | Bin 0 -> 4867 bytes ...bird-systemtray-update-connected-macos.png | Bin 0 -> 3570 bytes .../netbird-systemtray-update-connected.ico | Bin 7678 -> 104698 bytes .../netbird-systemtray-update-connected.png | Bin 11471 -> 4842 bytes ...rd-systemtray-update-disconnected-dark.ico | Bin 0 -> 105086 bytes ...rd-systemtray-update-disconnected-dark.png | Bin 0 -> 5275 bytes ...d-systemtray-update-disconnected-macos.png | Bin 0 -> 3816 bytes ...netbird-systemtray-update-disconnected.ico | Bin 7966 -> 105115 bytes ...netbird-systemtray-update-disconnected.png | Bin 12437 -> 5298 bytes client/ui/netbird.png | Bin 0 -> 4800 bytes 33 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 client/ui/netbird-systemtray-connected-dark.ico create mode 100644 client/ui/netbird-systemtray-connected-dark.png create mode 100644 client/ui/netbird-systemtray-connected-macos.png create mode 100644 client/ui/netbird-systemtray-connecting-dark.ico create mode 100644 client/ui/netbird-systemtray-connecting-dark.png create mode 100644 client/ui/netbird-systemtray-connecting-macos.png create mode 100644 client/ui/netbird-systemtray-connecting.ico create mode 100644 client/ui/netbird-systemtray-connecting.png create mode 100644 client/ui/netbird-systemtray-disconnected-macos.png create mode 100644 client/ui/netbird-systemtray-error-dark.ico create mode 100644 client/ui/netbird-systemtray-error-dark.png create mode 100644 client/ui/netbird-systemtray-error-macos.png create mode 100644 client/ui/netbird-systemtray-error.ico create mode 100644 client/ui/netbird-systemtray-error.png delete mode 100644 client/ui/netbird-systemtray-update-cloud.ico delete mode 100644 client/ui/netbird-systemtray-update-cloud.png create mode 100644 client/ui/netbird-systemtray-update-connected-dark.ico create mode 100644 client/ui/netbird-systemtray-update-connected-dark.png create mode 100644 client/ui/netbird-systemtray-update-connected-macos.png create mode 100644 client/ui/netbird-systemtray-update-disconnected-dark.ico create mode 100644 client/ui/netbird-systemtray-update-disconnected-dark.png create mode 100644 client/ui/netbird-systemtray-update-disconnected-macos.png create mode 100644 client/ui/netbird.png diff --git a/.goreleaser_ui.yaml b/.goreleaser_ui.yaml index 06577f4e3..983aa0e78 100644 --- a/.goreleaser_ui.yaml +++ b/.goreleaser_ui.yaml @@ -53,7 +53,7 @@ nfpms: contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop - - src: client/ui/netbird-systemtray-connected.png + - src: client/ui/netbird.png dst: /usr/share/pixmaps/netbird.png dependencies: - netbird @@ -70,7 +70,7 @@ nfpms: contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop - - src: client/ui/netbird-systemtray-connected.png + - src: client/ui/netbird.png dst: /usr/share/pixmaps/netbird.png dependencies: - netbird diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index f22ee377b..1aa61a2b2 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -21,6 +21,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "fyne.io/systray" "github.com/cenkalti/backoff/v4" @@ -90,6 +91,14 @@ func main() { } client := newServiceClient(daemonAddr, a, showSettings, showRoutes) + settingsChangeChan := make(chan fyne.Settings) + a.Settings().AddChangeListener(settingsChangeChan) + go func() { + for range settingsChangeChan { + client.updateIcon() + } + }() + if showSettings || showRoutes { a.Run() } else { @@ -106,46 +115,108 @@ func main() { } } +//go:embed netbird.ico +var iconAboutICO []byte + +//go:embed netbird.png +var iconAboutPNG []byte + //go:embed netbird-systemtray-connected.ico var iconConnectedICO []byte //go:embed netbird-systemtray-connected.png var iconConnectedPNG []byte +//go:embed netbird-systemtray-connected-macos.png +var iconConnectedMacOS []byte + +//go:embed netbird-systemtray-connected-dark.ico +var iconConnectedDarkICO []byte + +//go:embed netbird-systemtray-connected-dark.png +var iconConnectedDarkPNG []byte + //go:embed netbird-systemtray-disconnected.ico var iconDisconnectedICO []byte //go:embed netbird-systemtray-disconnected.png var iconDisconnectedPNG []byte +//go:embed netbird-systemtray-disconnected-macos.png +var iconDisconnectedMacOS []byte + //go:embed netbird-systemtray-update-disconnected.ico var iconUpdateDisconnectedICO []byte //go:embed netbird-systemtray-update-disconnected.png var iconUpdateDisconnectedPNG []byte +//go:embed netbird-systemtray-update-disconnected-macos.png +var iconUpdateDisconnectedMacOS []byte + +//go:embed netbird-systemtray-update-disconnected-dark.ico +var iconUpdateDisconnectedDarkICO []byte + +//go:embed netbird-systemtray-update-disconnected-dark.png +var iconUpdateDisconnectedDarkPNG []byte + //go:embed netbird-systemtray-update-connected.ico var iconUpdateConnectedICO []byte //go:embed netbird-systemtray-update-connected.png var iconUpdateConnectedPNG []byte -//go:embed netbird-systemtray-update-cloud.ico -var iconUpdateCloudICO []byte +//go:embed netbird-systemtray-update-connected-macos.png +var iconUpdateConnectedMacOS []byte -//go:embed netbird-systemtray-update-cloud.png -var iconUpdateCloudPNG []byte +//go:embed netbird-systemtray-update-connected-dark.ico +var iconUpdateConnectedDarkICO []byte + +//go:embed netbird-systemtray-update-connected-dark.png +var iconUpdateConnectedDarkPNG []byte + +//go:embed netbird-systemtray-connecting.ico +var iconConnectingICO []byte + +//go:embed netbird-systemtray-connecting.png +var iconConnectingPNG []byte + +//go:embed netbird-systemtray-connecting-macos.png +var iconConnectingMacOS []byte + +//go:embed netbird-systemtray-connecting-dark.ico +var iconConnectingDarkICO []byte + +//go:embed netbird-systemtray-connecting-dark.png +var iconConnectingDarkPNG []byte + +//go:embed netbird-systemtray-error.ico +var iconErrorICO []byte + +//go:embed netbird-systemtray-error.png +var iconErrorPNG []byte + +//go:embed netbird-systemtray-error-macos.png +var iconErrorMacOS []byte + +//go:embed netbird-systemtray-error-dark.ico +var iconErrorDarkICO []byte + +//go:embed netbird-systemtray-error-dark.png +var iconErrorDarkPNG []byte type serviceClient struct { ctx context.Context addr string conn proto.DaemonServiceClient + icAbout []byte icConnected []byte icDisconnected []byte icUpdateConnected []byte icUpdateDisconnected []byte - icUpdateCloud []byte + icConnecting []byte + icError []byte // systray menu items mStatus *systray.MenuItem @@ -214,20 +285,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo update: version.NewUpdate(), } - if runtime.GOOS == "windows" { - s.icConnected = iconConnectedICO - s.icDisconnected = iconDisconnectedICO - s.icUpdateConnected = iconUpdateConnectedICO - s.icUpdateDisconnected = iconUpdateDisconnectedICO - s.icUpdateCloud = iconUpdateCloudICO - - } else { - s.icConnected = iconConnectedPNG - s.icDisconnected = iconDisconnectedPNG - s.icUpdateConnected = iconUpdateConnectedPNG - s.icUpdateDisconnected = iconUpdateDisconnectedPNG - s.icUpdateCloud = iconUpdateCloudPNG - } + s.setNewIcons() if showSettings { s.showSettingsUI() @@ -239,6 +297,63 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo return s } +func (s *serviceClient) setNewIcons() { + if runtime.GOOS == "windows" { + s.icAbout = iconAboutICO + if s.app.Settings().ThemeVariant() == theme.VariantDark { + s.icConnected = iconConnectedDarkICO + s.icDisconnected = iconDisconnectedICO + s.icUpdateConnected = iconUpdateConnectedDarkICO + s.icUpdateDisconnected = iconUpdateDisconnectedDarkICO + s.icConnecting = iconConnectingDarkICO + s.icError = iconErrorDarkICO + } else { + s.icConnected = iconConnectedICO + s.icDisconnected = iconDisconnectedICO + s.icUpdateConnected = iconUpdateConnectedICO + s.icUpdateDisconnected = iconUpdateDisconnectedICO + s.icConnecting = iconConnectingICO + s.icError = iconErrorICO + } + } else { + s.icAbout = iconAboutPNG + if s.app.Settings().ThemeVariant() == theme.VariantDark { + s.icConnected = iconConnectedDarkPNG + s.icDisconnected = iconDisconnectedPNG + s.icUpdateConnected = iconUpdateConnectedDarkPNG + s.icUpdateDisconnected = iconUpdateDisconnectedDarkPNG + s.icConnecting = iconConnectingDarkPNG + s.icError = iconErrorDarkPNG + } else { + s.icConnected = iconConnectedPNG + s.icDisconnected = iconDisconnectedPNG + s.icUpdateConnected = iconUpdateConnectedPNG + s.icUpdateDisconnected = iconUpdateDisconnectedPNG + s.icConnecting = iconConnectingPNG + s.icError = iconErrorPNG + } + } +} + +func (s *serviceClient) updateIcon() { + s.setNewIcons() + s.updateIndicationLock.Lock() + if s.connected { + if s.isUpdateIconActive { + systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) + } else { + systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) + } + } else { + if s.isUpdateIconActive { + systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) + } else { + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) + } + } + s.updateIndicationLock.Unlock() +} + func (s *serviceClient) showSettingsUI() { // add settings window UI elements. s.wSettings = s.app.NewWindow("NetBird Settings") @@ -376,8 +491,10 @@ func (s *serviceClient) login() error { } func (s *serviceClient) menuUpClick() error { + systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { + systray.SetTemplateIcon(iconErrorMacOS, s.icError) log.Errorf("get client: %v", err) return err } @@ -407,6 +524,7 @@ func (s *serviceClient) menuUpClick() error { } func (s *serviceClient) menuDownClick() error { + systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { log.Errorf("get client: %v", err) @@ -458,9 +576,9 @@ func (s *serviceClient) updateStatus() error { s.connected = true s.sendNotification = true if s.isUpdateIconActive { - systray.SetIcon(s.icUpdateConnected) + systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) } else { - systray.SetIcon(s.icConnected) + systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) } systray.SetTooltip("NetBird (Connected)") s.mStatus.SetTitle("Connected") @@ -482,11 +600,9 @@ func (s *serviceClient) updateStatus() error { s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion) if !s.isUpdateIconActive { if systrayIconState { - systray.SetIcon(s.icConnected) - s.mAbout.SetIcon(s.icConnected) + systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) } else { - systray.SetIcon(s.icDisconnected) - s.mAbout.SetIcon(s.icDisconnected) + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) } } @@ -517,9 +633,9 @@ func (s *serviceClient) updateStatus() error { func (s *serviceClient) setDisconnectedStatus() { s.connected = false if s.isUpdateIconActive { - systray.SetIcon(s.icUpdateDisconnected) + systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) } else { - systray.SetIcon(s.icDisconnected) + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) } systray.SetTooltip("NetBird (Disconnected)") s.mStatus.SetTitle("Disconnected") @@ -529,7 +645,7 @@ func (s *serviceClient) setDisconnectedStatus() { } func (s *serviceClient) onTrayReady() { - systray.SetIcon(s.icDisconnected) + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) systray.SetTooltip("NetBird") // setup systray menu items @@ -554,7 +670,7 @@ func (s *serviceClient) onTrayReady() { systray.AddSeparator() s.mAbout = systray.AddMenuItem("About", "About") - s.mAbout.SetIcon(s.icDisconnected) + s.mAbout.SetIcon(s.icAbout) versionString := normalizedVersion(version.NetbirdVersion()) s.mVersionUI = s.mAbout.AddSubMenuItem(fmt.Sprintf("GUI: %s", versionString), fmt.Sprintf("GUI Version: %s", versionString)) s.mVersionUI.Disable() @@ -771,9 +887,9 @@ func (s *serviceClient) onUpdateAvailable() { s.isUpdateIconActive = true if s.connected { - systray.SetIcon(s.icUpdateConnected) + systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) } else { - systray.SetIcon(s.icUpdateDisconnected) + systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) } } diff --git a/client/ui/netbird-systemtray-connected-dark.ico b/client/ui/netbird-systemtray-connected-dark.ico new file mode 100644 index 0000000000000000000000000000000000000000..0db8a08624e6f0f0e679fff0e69d9d182f64e41b GIT binary patch literal 105144 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connected-dark.png b/client/ui/netbird-systemtray-connected-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f18a929a0c46fe6eb534932bb8af7dee80a15698 GIT binary patch literal 5272 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 b)IkE9NkTHqSjM4s{{jzZW@B1u>>2kzO3uwf literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connected-macos.png b/client/ui/netbird-systemtray-connected-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..ead210250d7a4b25461aa0311fcb6c13cec75057 GIT binary patch literal 3858 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connected.ico b/client/ui/netbird-systemtray-connected.ico index 80550aa37e20d9d134cda6c0cfdeb731f1765cfe..c16bec3f51e8e41d8263b2b4b5dc2095b51e1261 100644 GIT binary patch literal 105151 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

6A!aevjJ&cU%ypida~ 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 literal 5139 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 diff --git a/client/ui/netbird-systemtray-connecting-dark.ico b/client/ui/netbird-systemtray-connecting-dark.ico new file mode 100644 index 0000000000000000000000000000000000000000..615d40f075b41f3b3c9241611cc4b249d0851899 GIT binary patch literal 105128 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 OF~=dJ2Ok^g7W{u+(t)V} literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connecting-dark.png b/client/ui/netbird-systemtray-connecting-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a665eb61cc066b42388ce7acd0858d0321a7b708 GIT binary patch literal 5434 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connecting-macos.png b/client/ui/netbird-systemtray-connecting-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..0fe7fa0dbe50bcf8c08e45c7cf13b7e6e2348335 GIT binary patch literal 3843 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 OY{ZbUgZ~;x5BxvcE4?TH literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connecting.png b/client/ui/netbird-systemtray-connecting.png new file mode 100644 index 0000000000000000000000000000000000000000..4f607c997df29ed0104e0e824155f65a1098a3a5 GIT binary patch literal 5412 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-disconnected-macos.png b/client/ui/netbird-systemtray-disconnected-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..36b9a488f16673b1fcb1c87037d3d1606dfb1e5b GIT binary patch literal 3491 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 VFbHp#EYMSdj14UGKVEZ=`9J&Jr*QxP literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-disconnected.ico b/client/ui/netbird-systemtray-disconnected.ico index aa75268b0c7c110352eed1b76b21a97e8e24b909..dcb9f4bf83dcfcb18d4858d5cf553895b092eca0 100644 GIT binary patch literal 104575 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 Uw;PvXstfzveZt6u;jD=N1N46qGXMYp literal 5167 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 diff --git a/client/ui/netbird-systemtray-error-dark.ico b/client/ui/netbird-systemtray-error-dark.ico new file mode 100644 index 0000000000000000000000000000000000000000..083816188d695c0a083ba7dcd78c1f31e9f06080 GIT binary patch literal 105062 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-error-macos.png b/client/ui/netbird-systemtray-error-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..9a9998bcfd1f2f4b686f2c08ac4be42198111d83 GIT binary patch literal 3837 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 Z(%>Cos#cWtK%X^)%YJvqTKk}k{{iS8*lPd) literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-cloud.ico b/client/ui/netbird-systemtray-update-cloud.ico deleted file mode 100644 index b87c6f4b55620eea0b397d6243063e22315933f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3647 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 UPWN{{D&hjh2Il(Jy7=h-0rzr)wg3PC diff --git a/client/ui/netbird-systemtray-update-connected-dark.ico b/client/ui/netbird-systemtray-update-connected-dark.ico new file mode 100644 index 0000000000000000000000000000000000000000..b11bb54927fe232adf7924216fbbce9569d5156d GIT binary patch literal 104704 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-connected-dark.png b/client/ui/netbird-systemtray-update-connected-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..52ae621ac0f35138d31b68201d70940a61d947e1 GIT binary patch literal 4867 zcma)Ac{J4F*MDXV#y(^#gtCk!W2v7l&DfJ@gsd59p{&^{vq+Yav{_0RN>Yh7OTze} zjY0{f&}7Xr#yXlA-tWBszvsN)bLM>SbMNQ5+jH+d-+8j|u8tC-J468h5>5y0jsgIK zNEkp0K`;O4hi9OdNYp`}7y!h!@qaLI=dL0j5_8ni20Z<&JOeEd!PYL;06fbT8eyy4AI9+CmHjg5#$5Z-T)>ub~?SKqf~F16MOn%?cV!cWf&0@Kt1u9O>sBs z7OX{()js(#i>N0I$bdYi_;lSx&`N}TjQ|c(@d*(5lCN>hN(tIm_DK*0 z#`bkVWE)=v!0c-oF`d}(hmLOK{40oD_6N>jk=nJ|qEk8rM}kEMX9yhdnGa@lJXv>1 z+N*#AeJ z%%pkAqqqL>!`qRuN;Ef^f_uYp_i9HlJrjJrIc>?U^4c*leHh*1$@-qqD1))n;9&K(+5m~RZX5)>^!c#Q=7@Bb(Y!O;j2{VzyD!h}D zITI;A90HY}uE+f*XussTY@f~^nBW!MaCv^81;*f`ga>)TwjUcBEbTBc>a(rpp%pZx za`w%8?swQllIeG+2`cuycNzV;>dJ}DHLbJKdodLwZMioepQ7XeMaicW!1YtBy^ozJ`I#qcYAhlrONwG2i?g(c%WHL9b}tXA9{k|NR?|FX@S!!JA_S?mtEVsY2#;^Md;FIz)-fD_bzM@&D5Sj%y( zGyYi_

  • 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 JPi+EI{|6dE%h&(_ literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-connected-macos.png b/client/ui/netbird-systemtray-update-connected-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..8a6b2f2db85a6b6e312015ce634ec49096c45059 GIT binary patch literal 3570 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 MdJg!tzfZ*f0jyzlfdBvi literal 7678 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 My6&w?HM_9?0TFN?z5oCK diff --git a/client/ui/netbird-systemtray-update-connected.png b/client/ui/netbird-systemtray-update-connected.png index a0c4533406c399aacba6184ad7e74be9e20a0cf0..90bb0b7f1975c585b48a5088710678521a236ccd 100644 GIT binary patch literal 4842 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 WuRrb?z6NGGiIKzHh8`Q@9rAy92WzMR literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-disconnected-dark.png b/client/ui/netbird-systemtray-update-disconnected-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9e05351f1fe9e64bb78700907bb63875c0e8b5e1 GIT binary patch literal 5275 zcmbtYi8qvO*nfr@+vK%mE8EyZSt=yU%viD%#+J$&MUkblXUq`k#amjGB8DVUMhV$w zEXh`uP%<%O7iNrQhS|RH4}9l4=X=h1&i&ly{O;@bJJ)kx*L~gBzbI!rQIUfp004+O z*xR@P0EqVq0)zy3gU^lfVBR1cWqf1t)w%Z4px*+TKO&6ez*irMgR4s!9zS~;j z>#oIug5U!j`*m<>tQc$;oF-QN<=icxUF7tIbgMsOUv8@#_*NYbn7SCGG&(Uly2aqC z%Bc$Hq#3yX%6&Ha%Z5k)|9=c@#SyrB$xrdpT8~d7!wwAHQorid^75wkRnM09Ucxec zi^6;WI7ODp$oo-~ZHaY zJg~M@v>zdOuWlp8vzhr{V|yg*X`H<;8=rhM2}3)*0<>zkQ2$+v@*CJOuR=r!JJgq9 z{sRE#<~nNe{zfXjCn{u@E)L*4&oPutby-f<0QRhcD6O`{PHd!|h#WNCI(f~Pz4(!8 zazqX2^Sid7T+vC?t!A)W(}{*?%fHrRAgmFSzG1Gr=z2ZTQOvw89BwFgFE}|HI6UZu zevZyf`6qWg`SKVDp;qv>uaK~e-yd(cxwu<$UkyNnhh!uuEj3YF%^q!VvMdE{5#{Oi ztcnjTsVuU~a_xa{q0C$r0C71ZZbPt}-a?Q1HCj|GB=m*xmfN|E{uvN%{t^zK8PLFLXlD7dd4}Q+STyXeQ_FT<^B=}`D$6_=3eFu6I|`XH znak>XP0yi=Tu_wdHWwxN6zjYjSAUsZ`6?vwz<*zsyX5t+9;eKDgjO>;E`Hx`qK1?Q zEoAMa{n-5T?TY8zv&v^U`ev7l>}JKTqnFvU?cSM3rT<92hL7M!f3ACN1>s*O_alSm z^0ou_$qCYFc83@cs}|Xv!TWI_tSn<)_Ax;!4_0tPMJ3D=J4hbEkCOc0$Z?7!WC zLf2=gfCUQvik(TXBX`0t3{^4{EYB}K5;UATelFaar$>A<<8*4Q9@<>Nt*gI(8?GjF zr?K^S(#G)g{)&qczzJf-=?idD-AxstPMnNWtjIDEgX-*gp<6_-B)kbvT&G`eHc<}= zB1E`4Er4*Fv$-+tft26suyVzAitmazY7;63pLiWQO{s-rqWz_BF>LKd=*n@hk$%{t z&GglYHqJzn+Kd?!CTBi>I-D&yf^DtSKSTSdv17xYrAPkw%GIPQ38B96(-O7c7=mA9jh$ug>5Lj1%H3n3bC7-9l3pe-b$Wo-*Qns6M235 z(Z7)Jt+O6s*F(Lq?f&u4>)3iMbWQSpB+PlCHdQJ7I_yOAQ@HiC{$Zy*6@&z3ROCI< z^0##?%(3%+pdAbPF3Ym8OHo9gCnDW0MXYWkWjDs@ZrN?PvH*H96U z?+=`g9B)s7DTJR`7PqnGL^m?F2DoL(yJsM2lw%LIw>=w~H0wX6t=w;4q7g4-3EeJXX`8R2W7Bniedg0FwoA6W}%ejUq?>4<2REeXM|dIg*B zr@nK^sP`0Sq{7Z6x+=>jmFyP$xmD}VkhzNdxtzUIQSu#yk_H=(U=F@8%ZTWvq77!2qW!Fb~A7wu2;+;Cw4!q^Eo(fg&sKxI?>47GsOGnp&j1Tq;9W%@GW z%0a*F*5C=Poy{^bm5tGtxD~PdkwGQ_`K!ECa6IwoU~XHKOTl2w3Yg`|8vLHycw$uw zhZsvOZ0d;nPg(R1Y)XHo&Y!-Gl9QR$rh;DH_MdasMl7AS?cWtB!j7JxzF*-Y4==w#}K z#AMadpDD3-%`GJ_Ny+z$_>6|ebw4A48u0I%o{VEEcBKpZS|wZ7emGR({}IUi^s0VB z4~h}kY?T0~G{mDn>9L${-e_n=JlI}pC!25}SYaiLCoS&cXO_yTCf^*cd~wF1`$@^t z7qy}uRBmvfM1dPuFlLyylYB&t>WaU0{?b<@3fHHW2MX?7QaY6S#%jBV(tfp}*fu^> zdBs5~il8+E!})O(`E;X66b*>2U`m?HzB-Hf%1q~nA7q2jWS~ss{FFNhFG4u&N;qR6 zbV6>!(^d-1T%b+T^IBOfj&C;4)ctV%jh~je%taRyZ0owUrorPtKk@ZG^ow)&fG?~ zdrEd8bv>SzEn6&$#64HV)fH(^9EkD}i!yDpy7bXOi55&re`6KwlfAQ?ro-PIsVD%% zyq^Xc2cDtSqo>#3ta{CR(s+oh28E4Lh_6`Gb?AGXqUGHVN`22-P3^tMA>qWM`edg( zo#XOiyN6D#*MIvEr}3}us)Fo%aXzb{@4Rx@Oqo$()^yMse0sHMX)%JJ06PFvROL#RN1%L6z7MpMkK^U5^K_yei|F^uS3^==OfQ(~nCDXRITG?QAU#&g&e9-L>#E@O z^A2d75Ijr2x0fsVr}vk3+jE&YX4x`Sg3m@Lf!|SA)-hLJ+KA27NDNY-&smpB-Mz9b}x$oSyJ=`Is*FNgA)=QE`B`c7EZj(aGj+%UyV=ah0H#_w;m=GE2=iBsDLSoTgcW`_D1G1R;KEn%s zFxNRNyh1!OMgo}cOV)7kdm`o=u@fZ2I`kwjSeO^Md?w5h%`2sRvzjtQi=k#x;FvJ= z(M>lqTA1V9*sZRTPdgi39pzcXD%)e(;T10Ovxh;1-JtaWmCyGsuztR0tY$?J8!+$z zTcpbdxW7f_1Ik8r)Y8(jhYBN;m5DJyOJq!OGz?YdYKC?1%+C=l4nxc{_t2ly>RcD!njdkt=lqylbONoT=pqEZg8R%LtHOkK=FqD9Y@`ew!ft`g?j zoNfbB7zyOH{qRuP@#T|$i>0Br5-rO*&6+&5ms}lVK&1dwVd9%fDbCKrT6KZl(MNaE zaFay<=Sm{P?>4T=#uFlS(i?d+8u1^;-U=f1ByW=R@9-i*BnAWq_&v)+Y|-`pK0Aar z<$w9?K{^J{bfm76^e1_UG#&z*DFXrRxcRb)_g1{0+w+Xo?HS9A|9Gyr>Q0U2S(_i* zr?tr@(%!xR2zTeadwR*OMwjP2b61vm<_@0t-B0x3Hb_kG{gg63JTyRXu(i){;=6nL zr$yP0KVq{SbU$5e#JG8*b3lD!YMdYABn1Ow1>$ktj;HD~i5bKPRs?_l?maDcoJAUk znPQ$2=Zd3L$n`4SjX)?Mx&ynub+7u6*4{J{vMUcr&X^l!$1Y_XMHmfo;38Vr!Sz5fMn2V zAu&YQNp#~XW7x#*yg%87|ySiT`TN5~sKPD-IGO@?T zt}zdI8>7GrJCA<+K!fc;!PUREkJSa>glOn1m3mkJRWiWr`$gZak2ouiAc%1E;;VX-ZY+P)hj%mF?&I&a{60?((%d>Fdm7~+%% z0e@2bCq6>#0j0yrc$rTK2#KD?|E(nA6o+2^jYMZBDQ6p1Kyd;H#agx_e(ZzKCdIPqX}GL8oK&rs4r{> z7~DS%d&+>_B}>A!Fi)myVYJuXg4iY91hv*AOqP%CNrp+- z^ASkma_te!;U1vXi`(DN9H>u@H^Cqj-Hn9t1F*+b#fSCImg4+3R%V~eQHkID`5}Rq zzl7yq0&>uaj+TT35slYqc2U+%ynWpAU zb=7kF`(fR|MMR~t-~9fZ=GIJ*_vdNQ7hko%7|X6Z~$e& z&4FUFY}}=?4ap{#-|3;=^B67zgX!=Rd8tad%v6a5X4&%}kYy=$Z^=GDLYgBZ+;B(^ z&8sMQgqLP677sxHr44D%>BtAv%+wxU%~Zid%ka=5054tLt)|x7!))6gZ=l_!m-)Qw z2=`qf*v0YWV3xj!D%qs&quyXOkJ1UaTelk0PATDKh0OU;$hAE_VE}T!+BrYc7CFll z&0Xd@o0)vs^(+5Woxdh4uYzRiknaWmyX|1z;+qhZer54SA7$7aAjK09$j8J+&9};G z_f!Fe$-W|uJm$d6-0f52GB<%M?o@4rTR8lfpAAMB&|eJj{SAzlm68UXC&_y9z^H~>iA0e~C>Fn~={0RZ)V{~P50 d(wW0Lv-y*A*FK-s$qgR9OY2{tqh82QdHu literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-disconnected-macos.png b/client/ui/netbird-systemtray-update-disconnected-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..8b190034eac6588e58a36365880d83d9ea8d8db0 GIT binary patch literal 3816 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 literal 7966 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 diff --git a/client/ui/netbird-systemtray-update-disconnected.png b/client/ui/netbird-systemtray-update-disconnected.png index 3fbe88953aac6d630a053a45c5d8a51ea2a0e4e3..3adc3903436f78a5b7eab298c5e78d52a51d2fa7 100644 GIT binary patch literal 5298 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 Date: Tue, 18 Feb 2025 10:45:41 +0100 Subject: [PATCH 05/23] [misc] Run management benchmark jobs on file changes (#3343) They will always run on Main --- .github/workflows/golang-test-linux.yml | 12 ++++++++++++ management/README.md | 1 - 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index a4a3da66c..b1ec0a896 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -13,10 +13,19 @@ concurrency: jobs: build-cache: runs-on: ubuntu-22.04 + outputs: + management: ${{ steps.filter.outputs.management }} steps: - name: Checkout code uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + management: + - 'management/**' + - name: Install Go uses: actions/setup-go@v5 with: @@ -198,6 +207,7 @@ jobs: benchmark: needs: [ build-cache ] + if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: fail-fast: false matrix: @@ -258,6 +268,7 @@ jobs: api_benchmark: needs: [ build-cache ] + if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: fail-fast: false matrix: @@ -318,6 +329,7 @@ jobs: api_integration_test: needs: [ build-cache ] + if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: fail-fast: false matrix: diff --git a/management/README.md b/management/README.md index f0eb0cb70..1122a9e76 100644 --- a/management/README.md +++ b/management/README.md @@ -111,4 +111,3 @@ Generate gRpc code: #!/bin/bash protoc -I proto/ proto/management.proto --go_out=. --go-grpc_out=. ``` - From 50926bdbb4441f4b48d9ab341c41f203e082fc69 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:17:34 +0300 Subject: [PATCH 06/23] [client] [ui] issue when changing setting in GUI while peer session is expired (#3334) * [client] [ui] fix issue when changing settings in GUI while peer session is expired --- client/ui/client_ui.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 1aa61a2b2..30fb8d764 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -732,7 +732,6 @@ func (s *serviceClient) onTrayReady() { } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) - return } case <-s.mAutoConnect.ClickedCh: if s.mAutoConnect.Checked() { @@ -742,7 +741,6 @@ func (s *serviceClient) onTrayReady() { } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) - return } case <-s.mEnableRosenpass.ClickedCh: if s.mEnableRosenpass.Checked() { @@ -752,7 +750,6 @@ func (s *serviceClient) onTrayReady() { } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) - return } case <-s.mAdvancedSettings.ClickedCh: s.mAdvancedSettings.Disable() @@ -967,17 +964,20 @@ func (s *serviceClient) updateConfig() error { // restartClient restarts the client connection. func (s *serviceClient) restartClient(loginRequest *proto.LoginRequest) error { + ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout) + defer cancel() + client, err := s.getSrvClient(failFastTimeout) if err != nil { return err } - _, err = client.Login(s.ctx, loginRequest) + _, err = client.Login(ctx, loginRequest) if err != nil { return err } - _, err = client.Up(s.ctx, &proto.UpRequest{}) + _, err = client.Up(ctx, &proto.UpRequest{}) if err != nil { return err } From c974c12d652f276492f0e66f49aff08239cc5b2f Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:23:34 +0100 Subject: [PATCH 07/23] [signal] Fix registry not found (#3342) --- signal/server/signal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/signal/server/signal.go b/signal/server/signal.go index 05cc43276..3cae7e860 100644 --- a/signal/server/signal.go +++ b/signal/server/signal.go @@ -160,6 +160,7 @@ func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedM s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeNotConnected))) log.Debugf("message from peer [%s] can't be forwarded to peer [%s] because destination peer is not connected", msg.Key, msg.RemoteKey) // todo respond to the sender? + return } s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationFound))) From 2a864832c6820b2bf6c64d8b79fc83cc10950687 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:24:17 +0100 Subject: [PATCH 08/23] [management] remove gorm preparestmt from all DB connections (#3292) --- management/server/account_test.go | 16 ++++++++-------- management/server/geolocation/database.go | 1 - management/server/geolocation/store.go | 3 +-- management/server/migration/migration_test.go | 4 +--- management/server/store/sql_store.go | 13 ++++--------- management/server/store/store.go | 2 +- 6 files changed, 15 insertions(+), 24 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index 0a7f9119b..eb36dbd84 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -3018,11 +3018,11 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 1, 5, 3, 19}, - {"Medium", 500, 100, 7, 22, 10, 90}, - {"Large", 5000, 200, 65, 110, 60, 240}, + {"Small", 50, 5, 1, 5, 3, 24}, + {"Medium", 500, 100, 7, 22, 10, 135}, + {"Large", 5000, 200, 65, 110, 60, 320}, {"Small single", 50, 10, 1, 4, 3, 80}, - {"Medium single", 500, 10, 7, 13, 10, 37}, + {"Medium single", 500, 10, 7, 13, 10, 43}, {"Large 5", 5000, 15, 65, 80, 60, 220}, } @@ -3087,8 +3087,8 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { maxMsPerOpCICD float64 }{ {"Small", 50, 5, 2, 10, 3, 35}, - {"Medium", 500, 100, 5, 40, 20, 110}, - {"Large", 5000, 200, 60, 100, 120, 260}, + {"Medium", 500, 100, 5, 40, 20, 140}, + {"Large", 5000, 200, 60, 100, 120, 320}, {"Small single", 50, 10, 2, 10, 5, 40}, {"Medium single", 500, 10, 5, 40, 10, 60}, {"Large 5", 5000, 15, 60, 100, 60, 180}, @@ -3163,9 +3163,9 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) { }{ {"Small", 50, 5, 7, 20, 10, 80}, {"Medium", 500, 100, 5, 40, 30, 140}, - {"Large", 5000, 200, 80, 120, 140, 300}, + {"Large", 5000, 200, 80, 120, 140, 390}, {"Small single", 50, 10, 7, 20, 10, 80}, - {"Medium single", 500, 10, 5, 40, 20, 60}, + {"Medium single", 500, 10, 5, 40, 20, 85}, {"Large 5", 5000, 15, 80, 120, 80, 200}, } diff --git a/management/server/geolocation/database.go b/management/server/geolocation/database.go index 21ae93b9d..97ab398fb 100644 --- a/management/server/geolocation/database.go +++ b/management/server/geolocation/database.go @@ -123,7 +123,6 @@ func importCsvToSqlite(dataDir string, csvFile string, geonamesdbFile string) er db, err := gorm.Open(sqlite.Open(path.Join(dataDir, geonamesdbFile)), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), CreateBatchSize: 1000, - PrepareStmt: true, }) if err != nil { return err diff --git a/management/server/geolocation/store.go b/management/server/geolocation/store.go index 1f94bf47e..5af8276b5 100644 --- a/management/server/geolocation/store.go +++ b/management/server/geolocation/store.go @@ -132,8 +132,7 @@ func connectDB(ctx context.Context, filePath string) (*gorm.DB, error) { } db, err := gorm.Open(sqlite.Open(storeStr), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - PrepareStmt: true, + Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { return nil, err diff --git a/management/server/migration/migration_test.go b/management/server/migration/migration_test.go index a645ae325..e907d6853 100644 --- a/management/server/migration/migration_test.go +++ b/management/server/migration/migration_test.go @@ -21,9 +21,7 @@ import ( func setupDatabase(t *testing.T) *gorm.DB { t.Helper() - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{ - PrepareStmt: true, - }) + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) require.NoError(t, err, "Failed to open database") return db diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 5c4ddf666..947694420 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -969,7 +969,7 @@ func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMe } file := filepath.Join(dataDir, storeStr) - db, err := gorm.Open(sqlite.Open(file), getGormConfig(SqliteStoreEngine)) + db, err := gorm.Open(sqlite.Open(file), getGormConfig()) if err != nil { return nil, err } @@ -979,7 +979,7 @@ func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMe // NewPostgresqlStore creates a new Postgres store. func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) { - db, err := gorm.Open(postgres.Open(dsn), getGormConfig(PostgresStoreEngine)) + db, err := gorm.Open(postgres.Open(dsn), getGormConfig()) if err != nil { return nil, err } @@ -989,7 +989,7 @@ func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMe // NewMysqlStore creates a new MySQL store. func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) { - db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig(MysqlStoreEngine)) + db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig()) if err != nil { return nil, err } @@ -997,15 +997,10 @@ func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics return NewSqlStore(ctx, db, MysqlStoreEngine, metrics) } -func getGormConfig(engine Engine) *gorm.Config { - prepStmt := true - if engine == SqliteStoreEngine { - prepStmt = false - } +func getGormConfig() *gorm.Config { return &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), CreateBatchSize: 400, - PrepareStmt: prepStmt, } } diff --git a/management/server/store/store.go b/management/server/store/store.go index 6d3a409e6..29ed22fa5 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -328,7 +328,7 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( } file := filepath.Join(dataDir, storeStr) - db, err := gorm.Open(sqlite.Open(file), getGormConfig(kind)) + db, err := gorm.Open(sqlite.Open(file), getGormConfig()) if err != nil { return nil, nil, err } From 27b3891b14354f9c4bce3e9050fc9c9e43fcd431 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:35:30 +0100 Subject: [PATCH 09/23] [client] Set up local dns policy additionally if a gpo policy is detected (#3336) --- client/internal/dns/host_windows.go | 30 ++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 0cd078472..58b0a14de 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -131,11 +131,30 @@ func (r *registryConfigurator) addDNSSetupForAll(ip string) error { func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) error { // if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored // see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745 - policyPath := dnsPolicyConfigMatchPath if r.gpo { - policyPath = gpoDnsPolicyConfigMatchPath + if err := r.configureDNSPolicy(gpoDnsPolicyConfigMatchPath, domains, ip); err != nil { + return fmt.Errorf("configure GPO DNS policy: %w", err) + } + + if err := r.configureDNSPolicy(dnsPolicyConfigMatchPath, domains, ip); err != nil { + return fmt.Errorf("configure local DNS policy: %w", err) + } + + if err := refreshGroupPolicy(); err != nil { + log.Warnf("failed to refresh group policy: %v", err) + } + } else { + if err := r.configureDNSPolicy(dnsPolicyConfigMatchPath, domains, ip); err != nil { + return fmt.Errorf("configure local DNS policy: %w", err) + } } + log.Infof("added %d match domains. Domain list: %s", len(domains), domains) + return nil +} + +// configureDNSPolicy handles the actual configuration of a DNS policy at the specified path +func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip string) error { if err := removeRegistryKeyFromDNSPolicyConfig(policyPath); err != nil { return fmt.Errorf("remove existing dns policy: %w", err) } @@ -162,13 +181,6 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) er return fmt.Errorf("set %s: %w", dnsPolicyConfigConfigOptionsKey, err) } - if r.gpo { - if err := refreshGroupPolicy(); err != nil { - log.Warnf("failed to refresh group policy: %v", err) - } - } - - log.Infof("added %d match domains. Domain list: %s", len(domains), domains) return nil } From 7e6beee7f6bb18865c544c959f8dd5defe4b3762 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Wed, 19 Feb 2025 19:13:45 +0100 Subject: [PATCH 10/23] [management] optimize test execution (#3204) --- .github/workflows/golang-test-linux.yml | 150 ++- .golangci.yaml | 2 +- management/client/client_test.go | 5 +- management/server/dns_test.go | 14 +- management/server/group_test.go | 6 +- management/server/management_suite_test.go | 13 - management/server/management_test.go | 1046 ++++++++++++-------- management/server/nameserver_test.go | 13 +- management/server/route_test.go | 5 +- management/server/store/sql_store_test.go | 244 +++-- management/server/store/store.go | 152 ++- management/server/testutil/store.go | 4 +- management/server/types/user.go | 2 +- relay/client/dialer/ws/ws.go | 2 +- relay/server/listener/ws/conn.go | 2 +- relay/server/listener/ws/listener.go | 2 +- 16 files changed, 1019 insertions(+), 643 deletions(-) delete mode 100644 management/server/management_suite_test.go diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index b1ec0a896..efe1a2654 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -1,4 +1,4 @@ -name: Test Code Linux +name: Linux on: push: @@ -12,6 +12,7 @@ concurrency: jobs: build-cache: + name: "Build Cache" runs-on: ubuntu-22.04 outputs: management: ${{ steps.filter.outputs.management }} @@ -47,7 +48,6 @@ jobs: key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} - - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' @@ -98,6 +98,7 @@ jobs: run: CGO_ENABLED=1 GOARCH=386 go build -o relay-386 . test: + name: "Client / Unit" needs: [build-cache] strategy: fail-fast: false @@ -143,9 +144,116 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay) + + test_relay: + name: "Relay / Unit" + needs: [build-cache] + strategy: + fail-fast: false + matrix: + arch: [ '386','amd64' ] + runs-on: ubuntu-22.04 + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.x" + cache: false + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get Go environment + run: | + echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV + echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV + + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: | + ${{ env.cache }} + ${{ env.modcache }} + key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-gotest-cache- + + - name: Install dependencies + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev + + - name: Install 32-bit libpcap + if: matrix.arch == '386' + run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386 + + - name: Install modules + run: go mod tidy + + - name: check git status + run: git --no-pager diff --exit-code + + - name: Test + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + go test \ + -exec 'sudo' \ + -timeout 10m ./signal/... + + test_signal: + name: "Signal / Unit" + needs: [build-cache] + strategy: + fail-fast: false + matrix: + arch: [ '386','amd64' ] + runs-on: ubuntu-22.04 + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.x" + cache: false + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get Go environment + run: | + echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV + echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV + + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: | + ${{ env.cache }} + ${{ env.modcache }} + key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-gotest-cache- + + - name: Install dependencies + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev + + - name: Install 32-bit libpcap + if: matrix.arch == '386' + run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386 + + - name: Install modules + run: go mod tidy + + - name: check git status + run: git --no-pager diff --exit-code + + - name: Test + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + go test \ + -exec 'sudo' \ + -timeout 10m ./signal/... test_management: + name: "Management / Unit" needs: [ build-cache ] strategy: fail-fast: false @@ -203,9 +311,15 @@ jobs: run: docker pull mlsmaycon/warmed-mysql:8 - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=devcert -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management) + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} \ + go test -tags=devcert \ + -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE" \ + -timeout 10m ./management/... benchmark: + name: "Management / Benchmark" needs: [ build-cache ] if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: @@ -264,9 +378,15 @@ jobs: run: docker pull mlsmaycon/warmed-mysql:8 - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags devcert -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 20m ./... + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags devcert -run=^$ -bench=. \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ + -timeout 20m ./... api_benchmark: + name: "Management / Benchmark (API)" needs: [ build-cache ] if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: @@ -323,11 +443,19 @@ jobs: - name: download mysql image if: matrix.store == 'mysql' run: docker pull mlsmaycon/warmed-mysql:8 - + - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -run=^$ -tags=benchmark -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 30m $(go list -tags=benchmark ./... | grep /management) + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags=benchmark \ + -run=^$ \ + -bench=. \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ + -timeout 20m ./management/... api_integration_test: + name: "Management / Integration" needs: [ build-cache ] if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: @@ -375,9 +503,15 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=integration -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 30m $(go list -tags=integration ./... | grep /management) + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags=integration \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ + -timeout 10m ./management/... test_client_on_docker: + name: "Client (Docker) / Unit" needs: [ build-cache ] runs-on: ubuntu-20.04 steps: diff --git a/.golangci.yaml b/.golangci.yaml index 44b03d0e1..461677c2e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -103,7 +103,7 @@ linters: - predeclared # predeclared finds code that shadows one of Go's predeclared identifiers - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed - - thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers. + # - thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers. - wastedassign # wastedassign finds wasted assignment statements issues: # Maximum count of issues with the same text. diff --git a/management/client/client_test.go b/management/client/client_test.go index 3e498a5ea..b4ee58298 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -258,8 +258,11 @@ func TestClient_Sync(t *testing.T) { ch := make(chan *mgmtProto.SyncResponse, 1) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { - err = client.Sync(context.Background(), info, func(msg *mgmtProto.SyncResponse) error { + err = client.Sync(ctx, info, func(msg *mgmtProto.SyncResponse) error { ch <- msg return nil }) diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 6fb9f6a29..c40f62324 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -42,7 +42,7 @@ func TestGetDNSSettings(t *testing.T) { account, err := initTestDNSAccount(t, am) if err != nil { - t.Fatal("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } dnsSettings, err := am.GetDNSSettings(context.Background(), account.Id, dnsAdminUserID) @@ -124,12 +124,12 @@ func TestSaveDNSSettings(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { am, err := createDNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager") } account, err := initTestDNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %v", err) } err = am.SaveDNSSettings(context.Background(), account.Id, testCase.userID, testCase.inputSettings) @@ -156,22 +156,22 @@ func TestGetNetworkMap_DNSConfigSync(t *testing.T) { am, err := createDNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } account, err := initTestDNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } peer1, err := account.FindPeerByPubKey(dnsPeer1Key) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } peer2, err := account.FindPeerByPubKey(dnsPeer2Key) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } newAccountDNSConfig, err := am.GetNetworkMap(context.Background(), peer1.ID) diff --git a/management/server/group_test.go b/management/server/group_test.go index cc90f187b..b21b5e834 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -29,7 +29,7 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) { _, account, err := initTestGroupAccount(am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } for _, group := range account.Groups { group.Issued = types.GroupIssuedIntegration @@ -59,12 +59,12 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) { func TestDefaultAccountManager_DeleteGroup(t *testing.T) { am, err := createManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } _, account, err := initTestGroupAccount(am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } testCases := []struct { diff --git a/management/server/management_suite_test.go b/management/server/management_suite_test.go deleted file mode 100644 index cc99624a0..000000000 --- a/management/server/management_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package server_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestManagement(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Management Service Suite") -} diff --git a/management/server/management_test.go b/management/server/management_test.go index 43a6e40d5..1b91b3447 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -6,13 +6,13 @@ import ( "net" "os" "runtime" - sync2 "sync" + "sync" + "testing" "time" pb "github.com/golang/protobuf/proto" //nolint - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -30,424 +30,77 @@ import ( const ( ValidSetupKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB" - AccountKey = "bf1c8084-ba50-4ce7-9439-34653001fc3b" ) -var _ = Describe("Management service", func() { - var ( - addr string - s *grpc.Server - dataDir string - client mgmtProto.ManagementServiceClient - serverPubKey wgtypes.Key - conn *grpc.ClientConn - ) - - BeforeEach(func() { - level, _ := log.ParseLevel("Debug") - log.SetLevel(level) - var err error - dataDir, err = os.MkdirTemp("", "netbird_mgmt_test_tmp_*") - Expect(err).NotTo(HaveOccurred()) - - var listener net.Listener - - config := &server.Config{} - _, err = util.ReadJson("testdata/management.json", config) - Expect(err).NotTo(HaveOccurred()) - config.Datadir = dataDir - - s, listener = startServer(config, dataDir, "testdata/store.sql") - addr = listener.Addr().String() - client, conn = createRawClient(addr) - - // s public key - resp, err := client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) - Expect(err).NotTo(HaveOccurred()) - serverPubKey, err = wgtypes.ParseKey(resp.Key) - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - s.Stop() - err := conn.Close() - Expect(err).NotTo(HaveOccurred()) - os.RemoveAll(dataDir) - }) - - Context("when calling IsHealthy endpoint", func() { - Specify("a non-error result is returned", func() { - healthy, err := client.IsHealthy(context.TODO(), &mgmtProto.Empty{}) - - Expect(err).NotTo(HaveOccurred()) - Expect(healthy).ToNot(BeNil()) - }) - }) - - Context("when calling Sync endpoint", func() { - Context("when there is a new peer registered", func() { - Specify("a proper configuration is returned", func() { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - - syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} - encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp) - Expect(err).NotTo(HaveOccurred()) - - expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.netbird.io:10000", - Protocol: mgmtProto.HostConfig_HTTP, - } - expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.netbird.io:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - expectedTRUNHost := &mgmtProto.HostConfig{ - Uri: "turn:stun.netbird.io:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - - Expect(resp.NetbirdConfig.Signal).To(BeEquivalentTo(expectedSignalConfig)) - Expect(resp.NetbirdConfig.Stuns).To(ConsistOf(expectedStunsConfig)) - // TURN validation is special because credentials are dynamically generated - Expect(resp.NetbirdConfig.Turns).To(HaveLen(1)) - actualTURN := resp.NetbirdConfig.Turns[0] - Expect(len(actualTURN.User) > 0).To(BeTrue()) - Expect(actualTURN.HostConfig).To(BeEquivalentTo(expectedTRUNHost)) - Expect(len(resp.NetworkMap.OfflinePeers) == 0).To(BeTrue()) - }) - }) - - Context("when there are 3 peers registered under one account", func() { - Specify("a list containing other 2 peers is returned", func() { - key, _ := wgtypes.GenerateKey() - key1, _ := wgtypes.GenerateKey() - key2, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - loginPeerWithValidSetupKey(serverPubKey, key1, client) - loginPeerWithValidSetupKey(serverPubKey, key2, client) - - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(err).NotTo(HaveOccurred()) - - Expect(resp.GetRemotePeers()).To(HaveLen(2)) - peers := []string{resp.GetRemotePeers()[0].WgPubKey, resp.GetRemotePeers()[1].WgPubKey} - Expect(peers).To(ContainElements(key1.PublicKey().String(), key2.PublicKey().String())) - }) - }) - - Context("when there is a new peer registered", func() { - Specify("an update is returned", func() { - // register only a single peer - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - // after the initial sync call we have 0 peer updates - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(resp.GetRemotePeers()).To(HaveLen(0)) - - wg := sync2.WaitGroup{} - wg.Add(1) - - // continue listening on updates for a peer - go func() { - err = sync.RecvMsg(encryptedResponse) - - decryptedBytes, err = encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - resp = &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - wg.Done() - }() - - // register a new peer - key1, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key1, client) - - wg.Wait() - - Expect(err).NotTo(HaveOccurred()) - Expect(resp.GetRemotePeers()).To(HaveLen(1)) - Expect(resp.GetRemotePeers()[0].WgPubKey).To(BeEquivalentTo(key1.PublicKey().String())) - }) - }) - }) - - Context("when calling GetServerKey endpoint", func() { - Specify("a public Wireguard key of the service is returned", func() { - resp, err := client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) - - Expect(err).NotTo(HaveOccurred()) - Expect(resp).ToNot(BeNil()) - Expect(resp.Key).ToNot(BeNil()) - Expect(resp.ExpiresAt).ToNot(BeNil()) - - // check if the key is a valid Wireguard key - key, err := wgtypes.ParseKey(resp.Key) - Expect(err).NotTo(HaveOccurred()) - Expect(key).ToNot(BeNil()) - }) - }) - - Context("when calling Login endpoint", func() { - Context("with an invalid setup key", func() { - Specify("an error is returned", func() { - key, _ := wgtypes.GenerateKey() - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: "invalid setup key", - Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - - resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: message, - }) - - Expect(err).To(HaveOccurred()) - Expect(resp).To(BeNil()) - }) - }) - - Context("with a valid setup key", func() { - It("a non error result is returned", func() { - key, _ := wgtypes.GenerateKey() - resp := loginPeerWithValidSetupKey(serverPubKey, key, client) - - Expect(resp).ToNot(BeNil()) - }) - }) - - Context("with a registered peer", func() { - It("a non error result is returned", func() { - key, _ := wgtypes.GenerateKey() - regResp := loginPeerWithValidSetupKey(serverPubKey, key, client) - Expect(regResp).NotTo(BeNil()) - - // just login without registration - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - loginResp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: message, - }) - - Expect(err).NotTo(HaveOccurred()) - - decryptedResp := &mgmtProto.LoginResponse{} - err = encryption.DecryptMessage(serverPubKey, key, loginResp.Body, decryptedResp) - Expect(err).NotTo(HaveOccurred()) - - expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.netbird.io:10000", - Protocol: mgmtProto.HostConfig_HTTP, - } - expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.netbird.io:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{ - HostConfig: &mgmtProto.HostConfig{ - Uri: "turn:stun.netbird.io:3468", - Protocol: mgmtProto.HostConfig_UDP, - }, - User: "some_user", - Password: "some_password", - } - - Expect(decryptedResp.GetNetbirdConfig().Signal).To(BeEquivalentTo(expectedSignalConfig)) - Expect(decryptedResp.GetNetbirdConfig().Stuns).To(ConsistOf(expectedStunsConfig)) - Expect(decryptedResp.GetNetbirdConfig().Turns).To(ConsistOf(expectedTurnsConfig)) - }) - }) - }) - - Context("when there are 10 peers registered under one account", func() { - Context("when there are 10 more peers registered under the same account", func() { - Specify("all of the 10 peers will get updates of 10 newly registered peers", func() { - initialPeers := 10 - additionalPeers := 10 - - var peers []wgtypes.Key - for i := 0; i < initialPeers; i++ { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - peers = append(peers, key) - } - - wg := sync2.WaitGroup{} - wg.Add(initialPeers + initialPeers*additionalPeers) - - var clients []mgmtProto.ManagementService_SyncClient - for _, peer := range peers { - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, peer) - Expect(err).NotTo(HaveOccurred()) - - // open stream - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: peer.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - clients = append(clients, sync) - - // receive stream - peer := peer - go func() { - for { - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - if err != nil { - break - } - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, peer) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(err).NotTo(HaveOccurred()) - if len(resp.GetRemotePeers()) > 0 { - // only consider peer updates - wg.Done() - } - } - }() - } - - time.Sleep(1 * time.Second) - for i := 0; i < additionalPeers; i++ { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - r := rand.New(rand.NewSource(time.Now().UnixNano())) - n := r.Intn(200) - time.Sleep(time.Duration(n) * time.Millisecond) - } - - wg.Wait() - - for _, syncClient := range clients { - err := syncClient.CloseSend() - Expect(err).NotTo(HaveOccurred()) - } - }) - }) - }) - - Context("when there are peers registered under one account concurrently", func() { - Specify("then there are no duplicate IPs", func() { - initialPeers := 30 - - ipChannel := make(chan string, 20) - for i := 0; i < initialPeers; i++ { - go func() { - defer GinkgoRecover() - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} - encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq) - Expect(err).NotTo(HaveOccurred()) - - // open stream - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp) - Expect(err).NotTo(HaveOccurred()) - - ipChannel <- resp.GetPeerConfig().Address - }() - } - - ips := make(map[string]struct{}) - for ip := range ipChannel { - if _, ok := ips[ip]; ok { - Fail("found duplicate IP: " + ip) - } - ips[ip] = struct{}{} - if len(ips) == initialPeers { - break - } - } - close(ipChannel) - }) - }) - - Context("after login two peers", func() { - Specify("then they receive the same network", func() { - key, _ := wgtypes.GenerateKey() - firstLogin := loginPeerWithValidSetupKey(serverPubKey, key, client) - key, _ = wgtypes.GenerateKey() - secondLogin := loginPeerWithValidSetupKey(serverPubKey, key, client) - - _, firstLoginNetwork, err := net.ParseCIDR(firstLogin.GetPeerConfig().GetAddress()) - Expect(err).NotTo(HaveOccurred()) - _, secondLoginNetwork, err := net.ParseCIDR(secondLogin.GetPeerConfig().GetAddress()) - Expect(err).NotTo(HaveOccurred()) - - Expect(secondLoginNetwork.String()).To(BeEquivalentTo(firstLoginNetwork.String())) - }) - }) -}) - -func loginPeerWithValidSetupKey(serverPubKey wgtypes.Key, key wgtypes.Key, client mgmtProto.ManagementServiceClient) *mgmtProto.LoginResponse { - defer GinkgoRecover() - +type testSuite struct { + t *testing.T + addr string + grpcServer *grpc.Server + dataDir string + client mgmtProto.ManagementServiceClient + serverPubKey wgtypes.Key + conn *grpc.ClientConn +} + +func setupTest(t *testing.T) *testSuite { + t.Helper() + level, _ := log.ParseLevel("Debug") + log.SetLevel(level) + + ts := &testSuite{t: t} + + var err error + ts.dataDir, err = os.MkdirTemp("", "netbird_mgmt_test_tmp_*") + if err != nil { + t.Fatalf("failed to create temp directory: %v", err) + } + + config := &server.Config{} + _, err = util.ReadJson("testdata/management.json", config) + if err != nil { + t.Fatalf("failed to read management.json: %v", err) + } + config.Datadir = ts.dataDir + + var listener net.Listener + ts.grpcServer, listener = startServer(t, config, ts.dataDir, "testdata/store.sql") + ts.addr = listener.Addr().String() + + ts.client, ts.conn = createRawClient(t, ts.addr) + + resp, err := ts.client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("failed to get server key: %v", err) + } + + serverKey, err := wgtypes.ParseKey(resp.Key) + if err != nil { + t.Fatalf("failed to parse server key: %v", err) + } + ts.serverPubKey = serverKey + + return ts +} + +func tearDownTest(t *testing.T, ts *testSuite) { + t.Helper() + ts.grpcServer.Stop() + if err := ts.conn.Close(); err != nil { + t.Fatalf("failed to close client connection: %v", err) + } + time.Sleep(100 * time.Millisecond) + if err := os.RemoveAll(ts.dataDir); err != nil { + t.Fatalf("failed to remove data directory %s: %v", ts.dataDir, err) + } +} + +func loginPeerWithValidSetupKey( + t *testing.T, + serverPubKey wgtypes.Key, + key wgtypes.Key, + client mgmtProto.ManagementServiceClient, +) *mgmtProto.LoginResponse { + t.Helper() meta := &mgmtProto.PeerSystemMeta{ Hostname: key.PublicKey().String(), GoOS: runtime.GOOS, @@ -457,23 +110,30 @@ func loginPeerWithValidSetupKey(serverPubKey wgtypes.Key, key wgtypes.Key, clien Kernel: "kernel", NetbirdVersion: "", } - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta}) - Expect(err).NotTo(HaveOccurred()) + msgToEncrypt := &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta} + message, err := encryption.EncryptMessage(serverPubKey, key, msgToEncrypt) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ WgPubKey: key.PublicKey().String(), Body: message, }) - - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("login request failed: %v", err) + } loginResp := &mgmtProto.LoginResponse{} err = encryption.DecryptMessage(serverPubKey, key, resp.Body, loginResp) - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to decrypt login response: %v", err) + } return loginResp } -func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) { +func createRawClient(t *testing.T, addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) { + t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -484,17 +144,27 @@ func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.Clie Time: 10 * time.Second, Timeout: 2 * time.Second, })) - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to dial gRPC server: %v", err) + } return mgmtProto.NewManagementServiceClient(conn), conn } -func startServer(config *server.Config, dataDir string, testFile string) (*grpc.Server, net.Listener) { +func startServer( + t *testing.T, + config *server.Config, + dataDir string, + testFile string, +) (*grpc.Server, net.Listener) { + t.Helper() lis, err := net.Listen("tcp", ":0") - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to listen on a random port: %v", err) + } s := grpc.NewServer() - store, _, err := store.NewTestStoreFromSQL(context.Background(), testFile, dataDir) + str, _, err := store.NewTestStoreFromSQL(context.Background(), testFile, dataDir) if err != nil { log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) } @@ -504,23 +174,529 @@ func startServer(config *server.Config, dataDir string, testFile string) (*grpc. metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) if err != nil { - log.Fatalf("failed creating metrics: %v", err) + t.Fatalf("failed creating metrics: %v", err) } - accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, server.MocIntegratedValidator{}, metrics) + accountManager, err := server.BuildManager( + context.Background(), + str, + peersUpdateManager, + nil, + "", + "netbird.selfhosted", + eventStore, + nil, + false, + server.MocIntegratedValidator{}, + metrics, + ) if err != nil { - log.Fatalf("failed creating a manager: %v", err) + t.Fatalf("failed creating an account manager: %v", err) } secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil) - Expect(err).NotTo(HaveOccurred()) + mgmtServer, err := server.NewServer( + context.Background(), + config, + accountManager, + settings.NewManager(str), + peersUpdateManager, + secretsManager, + nil, + nil, + ) + if err != nil { + t.Fatalf("failed creating management server: %v", err) + } + mgmtProto.RegisterManagementServiceServer(s, mgmtServer) + go func() { if err := s.Serve(lis); err != nil { - Expect(err).NotTo(HaveOccurred()) + t.Errorf("failed to serve gRPC: %v", err) + return } }() return s, lis } + +func TestIsHealthy(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + healthy, err := ts.client.IsHealthy(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("IsHealthy call returned an error: %v", err) + } + if healthy == nil { + t.Fatal("IsHealthy returned a nil response") + } +} + +func TestSyncNewPeerConfiguration(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedBytes, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, syncReq) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive sync response message: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + err = encryption.DecryptMessage(ts.serverPubKey, peerKey, encryptedResponse.Body, resp) + if err != nil { + t.Fatalf("failed to decrypt sync response: %v", err) + } + + expectedSignalConfig := &mgmtProto.HostConfig{ + Uri: "signal.netbird.io:10000", + Protocol: mgmtProto.HostConfig_HTTP, + } + expectedStunsConfig := &mgmtProto.HostConfig{ + Uri: "stun:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + } + expectedTRUNHost := &mgmtProto.HostConfig{ + Uri: "turn:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + } + + assert.NotNil(t, resp.NetbirdConfig) + assert.Equal(t, resp.NetbirdConfig.Signal, expectedSignalConfig) + assert.Contains(t, resp.NetbirdConfig.Stuns, expectedStunsConfig) + assert.Equal(t, len(resp.NetbirdConfig.Turns), 1) + actualTURN := resp.NetbirdConfig.Turns[0] + assert.Greater(t, len(actualTURN.User), 0) + assert.Equal(t, actualTURN.HostConfig, expectedTRUNHost) + assert.Equal(t, len(resp.NetworkMap.OfflinePeers), 0) +} + +func TestSyncThreePeers(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + peerKey1, _ := wgtypes.GenerateKey() + peerKey2, _ := wgtypes.GenerateKey() + + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey1, ts.client) + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey2, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + syncBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal sync request: %v", err) + } + encryptedBytes, err := encryption.Encrypt(syncBytes, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive sync response: %v", err) + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to decrypt sync response: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + err = pb.Unmarshal(decryptedBytes, resp) + if err != nil { + t.Fatalf("failed to unmarshal sync response: %v", err) + } + + if len(resp.GetRemotePeers()) != 2 { + t.Fatalf("expected 2 remote peers, got %d", len(resp.GetRemotePeers())) + } + + var found1, found2 bool + for _, rp := range resp.GetRemotePeers() { + if rp.WgPubKey == peerKey1.PublicKey().String() { + found1 = true + } else if rp.WgPubKey == peerKey2.PublicKey().String() { + found2 = true + } + } + if !found1 || !found2 { + t.Fatalf("did not find the expected peer keys %s, %s among %v", + peerKey1.PublicKey().String(), + peerKey2.PublicKey().String(), + resp.GetRemotePeers()) + } +} + +func TestSyncNewPeerUpdate(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + syncBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal sync request: %v", err) + } + + encryptedBytes, err := encryption.Encrypt(syncBytes, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive first sync response: %v", err) + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to decrypt first sync response: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + if err := pb.Unmarshal(decryptedBytes, resp); err != nil { + t.Fatalf("failed to unmarshal first sync response: %v", err) + } + + if len(resp.GetRemotePeers()) != 0 { + t.Fatalf("expected 0 remote peers at first sync, got %d", len(resp.GetRemotePeers())) + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Errorf("failed to receive second sync response: %v", err) + return + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Errorf("failed to decrypt second sync response: %v", err) + return + } + err = pb.Unmarshal(decryptedBytes, resp) + if err != nil { + t.Errorf("failed to unmarshal second sync response: %v", err) + return + } + }() + + newPeerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, newPeerKey, ts.client) + + wg.Wait() + + if len(resp.GetRemotePeers()) != 1 { + t.Fatalf("expected exactly 1 remote peer update, got %d", len(resp.GetRemotePeers())) + } + if resp.GetRemotePeers()[0].WgPubKey != newPeerKey.PublicKey().String() { + t.Fatalf("expected new peer key %s, got %s", + newPeerKey.PublicKey().String(), + resp.GetRemotePeers()[0].WgPubKey) + } +} + +func TestGetServerKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + resp, err := ts.client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("GetServerKey returned error: %v", err) + } + if resp == nil { + t.Fatal("GetServerKey returned nil response") + } + if resp.Key == "" { + t.Fatal("GetServerKey returned empty key") + } + if resp.ExpiresAt.AsTime().IsZero() { + t.Fatal("GetServerKey returned 0 for ExpiresAt") + } + + _, err = wgtypes.ParseKey(resp.Key) + if err != nil { + t.Fatalf("GetServerKey returned an invalid WG key: %v", err) + } +} + +func TestLoginInvalidSetupKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + request := &mgmtProto.LoginRequest{ + SetupKey: "invalid setup key", + Meta: &mgmtProto.PeerSystemMeta{}, + } + encryptedMsg, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, request) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } + + resp, err := ts.client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedMsg, + }) + if err == nil { + t.Fatal("expected error for invalid setup key but got nil") + } + if resp != nil { + t.Fatalf("expected nil response for invalid setup key but got: %+v", resp) + } +} + +func TestLoginValidSetupKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + resp := loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + if resp == nil { + t.Fatal("loginPeerWithValidSetupKey returned nil, expected a valid response") + } +} + +func TestLoginRegisteredPeer(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + regResp := loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + if regResp == nil { + t.Fatal("registration with valid setup key failed") + } + + loginReq := &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedLogin, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, loginReq) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } + loginRespEnc, err := ts.client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedLogin, + }) + if err != nil { + t.Fatalf("login call returned an error: %v", err) + } + + loginResp := &mgmtProto.LoginResponse{} + err = encryption.DecryptMessage(ts.serverPubKey, peerKey, loginRespEnc.Body, loginResp) + if err != nil { + t.Fatalf("failed to decrypt login response: %v", err) + } + + expectedSignalConfig := &mgmtProto.HostConfig{ + Uri: "signal.netbird.io:10000", + Protocol: mgmtProto.HostConfig_HTTP, + } + expectedStunsConfig := &mgmtProto.HostConfig{ + Uri: "stun:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + } + expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{ + HostConfig: &mgmtProto.HostConfig{ + Uri: "turn:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + }, + User: "some_user", + Password: "some_password", + } + + assert.NotNil(t, loginResp.GetNetbirdConfig()) + assert.Equal(t, loginResp.GetNetbirdConfig().Signal, expectedSignalConfig) + assert.Contains(t, loginResp.GetNetbirdConfig().Stuns, expectedStunsConfig) + assert.Contains(t, loginResp.GetNetbirdConfig().Turns, expectedTurnsConfig) +} + +func TestSync10PeersGetUpdates(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + initialPeers := 10 + additionalPeers := 10 + + var peers []wgtypes.Key + for i := 0; i < initialPeers; i++ { + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + peers = append(peers, key) + } + + var wg sync.WaitGroup + wg.Add(initialPeers + initialPeers*additionalPeers) + + var syncClients []mgmtProto.ManagementService_SyncClient + for _, pk := range peers { + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + msgBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal SyncRequest: %v", err) + } + encBytes, err := encryption.Encrypt(msgBytes, ts.serverPubKey, pk) + if err != nil { + t.Fatalf("failed to encrypt SyncRequest: %v", err) + } + + s, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: pk.PublicKey().String(), + Body: encBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync for peer: %v", err) + } + syncClients = append(syncClients, s) + + go func(pk wgtypes.Key, syncStream mgmtProto.ManagementService_SyncClient) { + for { + encMsg := &mgmtProto.EncryptedMessage{} + err := syncStream.RecvMsg(encMsg) + if err != nil { + return + } + decryptedBytes, decErr := encryption.Decrypt(encMsg.Body, ts.serverPubKey, pk) + if decErr != nil { + t.Errorf("failed to decrypt SyncResponse for peer %s: %v", pk.PublicKey().String(), decErr) + return + } + resp := &mgmtProto.SyncResponse{} + umErr := pb.Unmarshal(decryptedBytes, resp) + if umErr != nil { + t.Errorf("failed to unmarshal SyncResponse for peer %s: %v", pk.PublicKey().String(), umErr) + return + } + // We only count if there's a new peer update + if len(resp.GetRemotePeers()) > 0 { + wg.Done() + } + } + }(pk, s) + } + + time.Sleep(500 * time.Millisecond) + for i := 0; i < additionalPeers; i++ { + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + r := rand.New(rand.NewSource(time.Now().UnixNano())) + n := r.Intn(200) + time.Sleep(time.Duration(n) * time.Millisecond) + } + + wg.Wait() + + for _, sc := range syncClients { + err := sc.CloseSend() + if err != nil { + t.Fatalf("failed to close sync client: %v", err) + } + } +} + +func TestConcurrentPeersNoDuplicateIPs(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + initialPeers := 30 + ipChan := make(chan string, initialPeers) + + var wg sync.WaitGroup + wg.Add(initialPeers) + + for i := 0; i < initialPeers; i++ { + go func() { + defer wg.Done() + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedBytes, err := encryption.EncryptMessage(ts.serverPubKey, key, syncReq) + if err != nil { + t.Errorf("failed to encrypt sync request: %v", err) + return + } + + s, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: key.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Errorf("failed to call Sync: %v", err) + return + } + + encResp := &mgmtProto.EncryptedMessage{} + if err = s.RecvMsg(encResp); err != nil { + t.Errorf("failed to receive sync response: %v", err) + return + } + + resp := &mgmtProto.SyncResponse{} + if err = encryption.DecryptMessage(ts.serverPubKey, key, encResp.Body, resp); err != nil { + t.Errorf("failed to decrypt sync response: %v", err) + return + } + ipChan <- resp.GetPeerConfig().Address + }() + } + + wg.Wait() + close(ipChan) + + ipMap := make(map[string]bool) + for ip := range ipChan { + if ipMap[ip] { + t.Fatalf("found duplicate IP: %s", ip) + } + ipMap[ip] = true + } + + // Ensure we collected all peers + if len(ipMap) != initialPeers { + t.Fatalf("expected %d unique IPs, got %d", initialPeers, len(ipMap)) + } +} diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 0743db513..497d9af4f 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -379,12 +379,12 @@ func TestCreateNameServerGroup(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { am, err := createNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } outNSGroup, err := am.CreateNameServerGroup( @@ -607,12 +607,12 @@ func TestSaveNameServerGroup(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { am, err := createNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } account.NameServerGroups[testCase.existingNSGroup.ID] = testCase.existingNSGroup @@ -706,7 +706,7 @@ func TestDeleteNameServerGroup(t *testing.T) { account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } account.NameServerGroups[testingNSGroup.ID] = testingNSGroup @@ -741,7 +741,7 @@ func TestGetNameServerGroup(t *testing.T) { account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } foundGroup, err := am.GetNameServerGroup(context.Background(), account.Id, testUserID, existingNSGroupID) @@ -761,6 +761,7 @@ func TestGetNameServerGroup(t *testing.T) { func createNSManager(t *testing.T) (*DefaultAccountManager, error) { t.Helper() + store, err := createNSStore(t) if err != nil { return nil, err diff --git a/management/server/route_test.go b/management/server/route_test.go index 1c5c56f60..40e0f41b0 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -13,12 +13,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/management/server/activity" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" - - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 6e04c7d9d..dd240ce6c 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -37,40 +37,44 @@ import ( nbroute "github.com/netbirdio/netbird/route" ) -func TestSqlite_NewStore(t *testing.T) { +func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { + t.Helper() + for _, engine := range supportedEngines { + if os.Getenv("NETBIRD_STORE_ENGINE") != "" && os.Getenv("NETBIRD_STORE_ENGINE") != string(engine) { + continue + } + t.Setenv("NETBIRD_STORE_ENGINE", string(engine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), testDataFile, t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) + t.Run(string(engine), func(t *testing.T) { + f(t, store) + }) + os.Unsetenv("NETBIRD_STORE_ENGINE") + } +} + +func Test_NewStore(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - if len(store.GetAllAccounts(context.Background())) != 0 { - t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") - } + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + if store == nil { + t.Errorf("expected to create a new Store") + } + if len(store.GetAllAccounts(context.Background())) != 0 { + t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") + } + }) } -func TestSqlite_SaveAccount_Large(t *testing.T) { +func Test_SaveAccount_Large(t *testing.T) { if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { t.Skip("skip CI tests on darwin and windows") } - t.Run("SQLite", func(t *testing.T) { - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - runLargeTest(t, store) - }) - - // create store outside to have a better time counter for the test - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - t.Run("PostgreSQL", func(t *testing.T) { + runTestForAllEngines(t, "", func(t *testing.T, store Store) { runLargeTest(t, store) }) } @@ -215,77 +219,74 @@ func randomIPv4() net.IP { return net.IP(b) } -func TestSqlite_SaveAccount(t *testing.T) { +func Test_SaveAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + account := newAccountWithId(context.Background(), "account_id", "testuser", "") + setupKey, _ := types.GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &nbpeer.Peer{ + Key: "peerkey", + IP: net.IP{127, 0, 0, 1}, + Meta: nbpeer.PeerSystemMeta{}, + Name: "peer name", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } - account := newAccountWithId(context.Background(), "account_id", "testuser", "") - setupKey, _ := types.GenerateDefaultSetupKey() - account.SetupKeys[setupKey.Key] = setupKey - account.Peers["testpeer"] = &nbpeer.Peer{ - Key: "peerkey", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + err := store.SaveAccount(context.Background(), account) + require.NoError(t, err) - err = store.SaveAccount(context.Background(), account) - require.NoError(t, err) + account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") + setupKey, _ = types.GenerateDefaultSetupKey() + account2.SetupKeys[setupKey.Key] = setupKey + account2.Peers["testpeer2"] = &nbpeer.Peer{ + Key: "peerkey2", + IP: net.IP{127, 0, 0, 2}, + Meta: nbpeer.PeerSystemMeta{}, + Name: "peer name 2", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } - account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") - setupKey, _ = types.GenerateDefaultSetupKey() - account2.SetupKeys[setupKey.Key] = setupKey - account2.Peers["testpeer2"] = &nbpeer.Peer{ - Key: "peerkey2", - IP: net.IP{127, 0, 0, 2}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name 2", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + err = store.SaveAccount(context.Background(), account2) + require.NoError(t, err) - err = store.SaveAccount(context.Background(), account2) - require.NoError(t, err) + if len(store.GetAllAccounts(context.Background())) != 2 { + t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") + } - if len(store.GetAllAccounts(context.Background())) != 2 { - t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") - } + a, err := store.GetAccount(context.Background(), account.Id) + if a == nil { + t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) + } - a, err := store.GetAccount(context.Background(), account.Id) - if a == nil { - t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) - } + if a != nil && len(a.Policies) != 1 { + t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies)) + } - if a != nil && len(a.Policies) != 1 { - t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies)) - } + if a != nil && len(a.Policies[0].Rules) != 1 { + t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules)) + return + } - if a != nil && len(a.Policies[0].Rules) != 1 { - t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules)) - return - } + if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil { + t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil { - t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { + t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { - t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { + t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { - t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) - } - - if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { - t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { + t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) + } + }) } func TestSqlite_DeleteAccount(t *testing.T) { @@ -402,27 +403,24 @@ func TestSqlite_DeleteAccount(t *testing.T) { } } -func TestSqlite_GetAccount(t *testing.T) { +func Test_GetAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + account, err := store.GetAccount(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, account.Id, "account id should match") - account, err := store.GetAccount(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, account.Id, "account id should match") - - _, err = store.GetAccount(context.Background(), "non-existing-account") - assert.Error(t, err) - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + _, err = store.GetAccount(context.Background(), "non-existing-account") + assert.Error(t, err) + parsedErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + }) } func TestSqlStore_SavePeer(t *testing.T) { @@ -580,51 +578,45 @@ func TestSqlStore_SavePeerLocation(t *testing.T) { require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } -func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) { +func Test_TestGetAccountByPrivateDomain(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + existingDomain := "test.com" - existingDomain := "test.com" + account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) + require.NoError(t, err, "should found account") + require.Equal(t, existingDomain, account.Domain, "domains should match") - account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) - require.NoError(t, err, "should found account") - require.Equal(t, existingDomain, account.Domain, "domains should match") - - _, err = store.GetAccountByPrivateDomain(context.Background(), "missing-domain.com") - require.Error(t, err, "should return error on domain lookup") - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + _, err = store.GetAccountByPrivateDomain(context.Background(), "missing-domain.com") + require.Error(t, err, "should return error on domain lookup") + parsedErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + }) } -func TestSqlite_GetTokenIDByHashedToken(t *testing.T) { +func Test_GetTokenIDByHashedToken(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + hashed := "SoMeHaShEdToKeN" + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - hashed := "SoMeHaShEdToKeN" - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) + require.NoError(t, err) + require.Equal(t, id, token) - token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) - require.NoError(t, err) - require.Equal(t, id, token) - - _, err = store.GetTokenIDByHashedToken(context.Background(), "non-existing-hash") - require.Error(t, err) - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + _, err = store.GetTokenIDByHashedToken(context.Background(), "non-existing-hash") + require.Error(t, err) + parsedErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + }) } func TestMigrate(t *testing.T) { diff --git a/management/server/store/store.go b/management/server/store/store.go index 29ed22fa5..e074c4c60 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -9,11 +9,16 @@ import ( "os" "path" "path/filepath" + "regexp" "runtime" + "slices" "strings" "time" + "github.com/google/uuid" log "github.com/sirupsen/logrus" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -193,6 +198,8 @@ const ( mysqlDsnEnv = "NETBIRD_STORE_ENGINE_MYSQL_DSN" ) +var supportedEngines = []Engine{SqliteStoreEngine, PostgresStoreEngine, MysqlStoreEngine} + func getStoreEngineFromEnv() Engine { // NETBIRD_STORE_ENGINE supposed to be used in tests. Otherwise, rely on the config file. kind, ok := os.LookupEnv("NETBIRD_STORE_ENGINE") @@ -201,7 +208,7 @@ func getStoreEngineFromEnv() Engine { } value := Engine(strings.ToLower(kind)) - if value == SqliteStoreEngine || value == PostgresStoreEngine || value == MysqlStoreEngine { + if slices.Contains(supportedEngines, value) { return value } @@ -349,51 +356,126 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( } func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) { - if kind == PostgresStoreEngine { - cleanUp, err := testutil.CreatePostgresTestContainer() - if err != nil { - return nil, nil, err + var cleanup func() + var err error + switch kind { + case PostgresStoreEngine: + store, cleanup, err = newReusedPostgresStore(ctx, store, kind) + case MysqlStoreEngine: + store, cleanup, err = newReusedMysqlStore(ctx, store, kind) + default: + cleanup = func() { + // sqlite doesn't need to be cleaned up } - - dsn, ok := os.LookupEnv(postgresDsnEnv) - if !ok { - return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv) - } - - store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) - if err != nil { - return nil, nil, err - } - - return store, cleanUp, nil } - - if kind == MysqlStoreEngine { - cleanUp, err := testutil.CreateMysqlTestContainer() - if err != nil { - return nil, nil, err - } - - dsn, ok := os.LookupEnv(mysqlDsnEnv) - if !ok { - return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) - } - - store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) - if err != nil { - return nil, nil, err - } - - return store, cleanUp, nil + if err != nil { + return nil, cleanup, fmt.Errorf("failed to create test store: %v", err) } closeConnection := func() { + cleanup() store.Close(ctx) } return store, closeConnection, nil } +func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind Engine) (*SqlStore, func(), error) { + if envDsn, ok := os.LookupEnv(postgresDsnEnv); !ok || envDsn == "" { + var err error + _, err = testutil.CreatePostgresTestContainer() + if err != nil { + return nil, nil, err + } + } + + dsn, ok := os.LookupEnv(postgresDsnEnv) + if !ok { + return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv) + } + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, nil, fmt.Errorf("failed to open postgres connection: %v", err) + } + + dsn, cleanup, err := createRandomDB(dsn, db, kind) + if err != nil { + return nil, cleanup, err + } + + store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) + if err != nil { + return nil, cleanup, err + } + + return store, cleanup, nil +} + +func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind Engine) (*SqlStore, func(), error) { + if envDsn, ok := os.LookupEnv(mysqlDsnEnv); !ok || envDsn == "" { + var err error + _, err = testutil.CreateMysqlTestContainer() + if err != nil { + return nil, nil, err + } + } + + dsn, ok := os.LookupEnv(mysqlDsnEnv) + if !ok { + return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) + } + + db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{}) + if err != nil { + return nil, nil, fmt.Errorf("failed to open mysql connection: %v", err) + } + + dsn, cleanup, err := createRandomDB(dsn, db, kind) + if err != nil { + return nil, cleanup, err + } + + store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) + if err != nil { + return nil, nil, err + } + + return store, cleanup, nil +} + +func createRandomDB(dsn string, db *gorm.DB, engine Engine) (string, func(), error) { + dbName := fmt.Sprintf("test_db_%s", strings.ReplaceAll(uuid.New().String(), "-", "_")) + + if err := db.Exec(fmt.Sprintf("CREATE DATABASE %s", dbName)).Error; err != nil { + return "", nil, fmt.Errorf("failed to create database: %v", err) + } + + var err error + cleanup := func() { + switch engine { + case PostgresStoreEngine: + err = db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)).Error + case MysqlStoreEngine: + // err = killMySQLConnections(dsn, dbName) + err = db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)).Error + } + if err != nil { + log.Errorf("failed to drop database %s: %v", dbName, err) + panic(err) + } + sqlDB, _ := db.DB() + _ = sqlDB.Close() + } + + return replaceDBName(dsn, dbName), cleanup, nil +} + +func replaceDBName(dsn, newDBName string) string { + re := regexp.MustCompile(`(?P
    [:/@])(?P[^/?]+)(?P\?|$)`)
    +	return re.ReplaceAllString(dsn, `${pre}`+newDBName+`${post}`)
    +}
    +
     func loadSQL(db *gorm.DB, filepath string) error {
     	sqlContent, err := os.ReadFile(filepath)
     	if err != nil {
    diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go
    index 16438cab8..8672efa7f 100644
    --- a/management/server/testutil/store.go
    +++ b/management/server/testutil/store.go
    @@ -22,7 +22,7 @@ func CreateMysqlTestContainer() (func(), error) {
     	myContainer, err := mysql.RunContainer(ctx,
     		testcontainers.WithImage("mlsmaycon/warmed-mysql:8"),
     		mysql.WithDatabase("testing"),
    -		mysql.WithUsername("testing"),
    +		mysql.WithUsername("root"),
     		mysql.WithPassword("testing"),
     		testcontainers.WithWaitStrategy(
     			wait.ForLog("/usr/sbin/mysqld: ready for connections").
    @@ -34,6 +34,7 @@ func CreateMysqlTestContainer() (func(), error) {
     	}
     
     	cleanup := func() {
    +		os.Unsetenv("NETBIRD_STORE_ENGINE_MYSQL_DSN")
     		timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
     		defer cancelFunc()
     		if err = myContainer.Terminate(timeoutCtx); err != nil {
    @@ -68,6 +69,7 @@ func CreatePostgresTestContainer() (func(), error) {
     	}
     
     	cleanup := func() {
    +		os.Unsetenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN")
     		timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
     		defer cancelFunc()
     		if err = pgContainer.Terminate(timeoutCtx); err != nil {
    diff --git a/management/server/types/user.go b/management/server/types/user.go
    index 348fbfb22..5f7a4f2cb 100644
    --- a/management/server/types/user.go
    +++ b/management/server/types/user.go
    @@ -80,7 +80,7 @@ type User struct {
     	// AutoGroups is a list of Group IDs to auto-assign to peers registered by this user
     	AutoGroups []string                        `gorm:"serializer:json"`
     	PATs       map[string]*PersonalAccessToken `gorm:"-"`
    -	PATsG      []PersonalAccessToken           `json:"-" gorm:"foreignKey:UserID;references:id"`
    +	PATsG      []PersonalAccessToken           `json:"-" gorm:"foreignKey:UserID;references:id;constraint:OnDelete:CASCADE;"`
     	// Blocked indicates whether the user is blocked. Blocked users can't use the system.
     	Blocked bool
     	// LastLogin is the last time the user logged in to IdP
    diff --git a/relay/client/dialer/ws/ws.go b/relay/client/dialer/ws/ws.go
    index b007e24bb..cb525865b 100644
    --- a/relay/client/dialer/ws/ws.go
    +++ b/relay/client/dialer/ws/ws.go
    @@ -11,8 +11,8 @@ import (
     	"net/url"
     	"strings"
     
    -	log "github.com/sirupsen/logrus"
     	"github.com/coder/websocket"
    +	log "github.com/sirupsen/logrus"
     
     	"github.com/netbirdio/netbird/relay/server/listener/ws"
     	"github.com/netbirdio/netbird/util/embeddedroots"
    diff --git a/relay/server/listener/ws/conn.go b/relay/server/listener/ws/conn.go
    index 3466b2abd..3ec08945b 100644
    --- a/relay/server/listener/ws/conn.go
    +++ b/relay/server/listener/ws/conn.go
    @@ -8,8 +8,8 @@ import (
     	"sync"
     	"time"
     
    -	log "github.com/sirupsen/logrus"
     	"github.com/coder/websocket"
    +	log "github.com/sirupsen/logrus"
     )
     
     const (
    diff --git a/relay/server/listener/ws/listener.go b/relay/server/listener/ws/listener.go
    index 4597669dc..3a95951ee 100644
    --- a/relay/server/listener/ws/listener.go
    +++ b/relay/server/listener/ws/listener.go
    @@ -8,8 +8,8 @@ import (
     	"net"
     	"net/http"
     
    -	log "github.com/sirupsen/logrus"
     	"github.com/coder/websocket"
    +	log "github.com/sirupsen/logrus"
     )
     
     // URLPath is the path for the websocket connection.
    
    From 33cf9535b328c06d27385e5476653a40aba0fd84 Mon Sep 17 00:00:00 2001
    From: Carlos Hernandez 
    Date: Thu, 20 Feb 2025 02:55:44 -0700
    Subject: [PATCH 11/23] [client] Use go build to embed less icons (#3351)
    
    ---
     client/ui/client_ui.go     | 122 +++++--------------------------------
     client/ui/icons.go         |  43 +++++++++++++
     client/ui/icons_windows.go |  41 +++++++++++++
     3 files changed, 99 insertions(+), 107 deletions(-)
     create mode 100644 client/ui/icons.go
     create mode 100644 client/ui/icons_windows.go
    
    diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go
    index 30fb8d764..618160128 100644
    --- a/client/ui/client_ui.go
    +++ b/client/ui/client_ui.go
    @@ -83,7 +83,7 @@ func main() {
     	}
     
     	a := app.NewWithID("NetBird")
    -	a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnectedPNG))
    +	a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnected))
     
     	if errorMSG != "" {
     		showErrorMSG(errorMSG)
    @@ -115,96 +115,24 @@ func main() {
     	}
     }
     
    -//go:embed netbird.ico
    -var iconAboutICO []byte
    -
    -//go:embed netbird.png
    -var iconAboutPNG []byte
    -
    -//go:embed netbird-systemtray-connected.ico
    -var iconConnectedICO []byte
    -
    -//go:embed netbird-systemtray-connected.png
    -var iconConnectedPNG []byte
    -
     //go:embed netbird-systemtray-connected-macos.png
     var iconConnectedMacOS []byte
     
    -//go:embed netbird-systemtray-connected-dark.ico
    -var iconConnectedDarkICO []byte
    -
    -//go:embed netbird-systemtray-connected-dark.png
    -var iconConnectedDarkPNG []byte
    -
    -//go:embed netbird-systemtray-disconnected.ico
    -var iconDisconnectedICO []byte
    -
    -//go:embed netbird-systemtray-disconnected.png
    -var iconDisconnectedPNG []byte
    -
     //go:embed netbird-systemtray-disconnected-macos.png
     var iconDisconnectedMacOS []byte
     
    -//go:embed netbird-systemtray-update-disconnected.ico
    -var iconUpdateDisconnectedICO []byte
    -
    -//go:embed netbird-systemtray-update-disconnected.png
    -var iconUpdateDisconnectedPNG []byte
    -
     //go:embed netbird-systemtray-update-disconnected-macos.png
     var iconUpdateDisconnectedMacOS []byte
     
    -//go:embed netbird-systemtray-update-disconnected-dark.ico
    -var iconUpdateDisconnectedDarkICO []byte
    -
    -//go:embed netbird-systemtray-update-disconnected-dark.png
    -var iconUpdateDisconnectedDarkPNG []byte
    -
    -//go:embed netbird-systemtray-update-connected.ico
    -var iconUpdateConnectedICO []byte
    -
    -//go:embed netbird-systemtray-update-connected.png
    -var iconUpdateConnectedPNG []byte
    -
     //go:embed netbird-systemtray-update-connected-macos.png
     var iconUpdateConnectedMacOS []byte
     
    -//go:embed netbird-systemtray-update-connected-dark.ico
    -var iconUpdateConnectedDarkICO []byte
    -
    -//go:embed netbird-systemtray-update-connected-dark.png
    -var iconUpdateConnectedDarkPNG []byte
    -
    -//go:embed netbird-systemtray-connecting.ico
    -var iconConnectingICO []byte
    -
    -//go:embed netbird-systemtray-connecting.png
    -var iconConnectingPNG []byte
    -
     //go:embed netbird-systemtray-connecting-macos.png
     var iconConnectingMacOS []byte
     
    -//go:embed netbird-systemtray-connecting-dark.ico
    -var iconConnectingDarkICO []byte
    -
    -//go:embed netbird-systemtray-connecting-dark.png
    -var iconConnectingDarkPNG []byte
    -
    -//go:embed netbird-systemtray-error.ico
    -var iconErrorICO []byte
    -
    -//go:embed netbird-systemtray-error.png
    -var iconErrorPNG []byte
    -
     //go:embed netbird-systemtray-error-macos.png
     var iconErrorMacOS []byte
     
    -//go:embed netbird-systemtray-error-dark.ico
    -var iconErrorDarkICO []byte
    -
    -//go:embed netbird-systemtray-error-dark.png
    -var iconErrorDarkPNG []byte
    -
     type serviceClient struct {
     	ctx  context.Context
     	addr string
    @@ -298,40 +226,21 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo
     }
     
     func (s *serviceClient) setNewIcons() {
    -	if runtime.GOOS == "windows" {
    -		s.icAbout = iconAboutICO
    -		if s.app.Settings().ThemeVariant() == theme.VariantDark {
    -			s.icConnected = iconConnectedDarkICO
    -			s.icDisconnected = iconDisconnectedICO
    -			s.icUpdateConnected = iconUpdateConnectedDarkICO
    -			s.icUpdateDisconnected = iconUpdateDisconnectedDarkICO
    -			s.icConnecting = iconConnectingDarkICO
    -			s.icError = iconErrorDarkICO
    -		} else {
    -			s.icConnected = iconConnectedICO
    -			s.icDisconnected = iconDisconnectedICO
    -			s.icUpdateConnected = iconUpdateConnectedICO
    -			s.icUpdateDisconnected = iconUpdateDisconnectedICO
    -			s.icConnecting = iconConnectingICO
    -			s.icError = iconErrorICO
    -		}
    +	s.icAbout = iconAbout
    +	if s.app.Settings().ThemeVariant() == theme.VariantDark {
    +		s.icConnected = iconConnectedDark
    +		s.icDisconnected = iconDisconnected
    +		s.icUpdateConnected = iconUpdateConnectedDark
    +		s.icUpdateDisconnected = iconUpdateDisconnectedDark
    +		s.icConnecting = iconConnectingDark
    +		s.icError = iconErrorDark
     	} else {
    -		s.icAbout = iconAboutPNG
    -		if s.app.Settings().ThemeVariant() == theme.VariantDark {
    -			s.icConnected = iconConnectedDarkPNG
    -			s.icDisconnected = iconDisconnectedPNG
    -			s.icUpdateConnected = iconUpdateConnectedDarkPNG
    -			s.icUpdateDisconnected = iconUpdateDisconnectedDarkPNG
    -			s.icConnecting = iconConnectingDarkPNG
    -			s.icError = iconErrorDarkPNG
    -		} else {
    -			s.icConnected = iconConnectedPNG
    -			s.icDisconnected = iconDisconnectedPNG
    -			s.icUpdateConnected = iconUpdateConnectedPNG
    -			s.icUpdateDisconnected = iconUpdateDisconnectedPNG
    -			s.icConnecting = iconConnectingPNG
    -			s.icError = iconErrorPNG
    -		}
    +		s.icConnected = iconConnected
    +		s.icDisconnected = iconDisconnected
    +		s.icUpdateConnected = iconUpdateConnected
    +		s.icUpdateDisconnected = iconUpdateDisconnected
    +		s.icConnecting = iconConnecting
    +		s.icError = iconError
     	}
     }
     
    @@ -622,7 +531,6 @@ func (s *serviceClient) updateStatus() error {
     		Stop:                backoff.Stop,
     		Clock:               backoff.SystemClock,
     	})
    -
     	if err != nil {
     		return err
     	}
    diff --git a/client/ui/icons.go b/client/ui/icons.go
    new file mode 100644
    index 000000000..6f3a9dbc9
    --- /dev/null
    +++ b/client/ui/icons.go
    @@ -0,0 +1,43 @@
    +//go:build !(linux && 386) && !windows
    +
    +package main
    +
    +import (
    +	_ "embed"
    +)
    +
    +//go:embed netbird.png
    +var iconAbout []byte
    +
    +//go:embed netbird-systemtray-connected.png
    +var iconConnected []byte
    +
    +//go:embed netbird-systemtray-connected-dark.png
    +var iconConnectedDark []byte
    +
    +//go:embed netbird-systemtray-disconnected.png
    +var iconDisconnected []byte
    +
    +//go:embed netbird-systemtray-update-disconnected.png
    +var iconUpdateDisconnected []byte
    +
    +//go:embed netbird-systemtray-update-disconnected-dark.png
    +var iconUpdateDisconnectedDark []byte
    +
    +//go:embed netbird-systemtray-update-connected.png
    +var iconUpdateConnected []byte
    +
    +//go:embed netbird-systemtray-update-connected-dark.png
    +var iconUpdateConnectedDark []byte
    +
    +//go:embed netbird-systemtray-connecting.png
    +var iconConnecting []byte
    +
    +//go:embed netbird-systemtray-connecting-dark.png
    +var iconConnectingDark []byte
    +
    +//go:embed netbird-systemtray-error.png
    +var iconError []byte
    +
    +//go:embed netbird-systemtray-error-dark.png
    +var iconErrorDark []byte
    diff --git a/client/ui/icons_windows.go b/client/ui/icons_windows.go
    new file mode 100644
    index 000000000..a2a924763
    --- /dev/null
    +++ b/client/ui/icons_windows.go
    @@ -0,0 +1,41 @@
    +package main
    +
    +import (
    + _ "embed"
    +)
    +
    +//go:embed netbird.ico
    +var iconAbout []byte
    +
    +//go:embed netbird-systemtray-connected.ico
    +var iconConnected []byte
    +
    +//go:embed netbird-systemtray-connected-dark.ico
    +var iconConnectedDark []byte
    +
    +//go:embed netbird-systemtray-disconnected.ico
    +var iconDisconnected []byte
    +
    +//go:embed netbird-systemtray-update-disconnected.ico
    +var iconUpdateDisconnected []byte
    +
    +//go:embed netbird-systemtray-update-disconnected-dark.ico
    +var iconUpdateDisconnectedDark []byte
    +
    +//go:embed netbird-systemtray-update-connected.ico
    +var iconUpdateConnected []byte
    +
    +//go:embed netbird-systemtray-update-connected-dark.ico
    +var iconUpdateConnectedDark []byte
    +
    +//go:embed netbird-systemtray-connecting.ico
    +var iconConnecting []byte
    +
    +//go:embed netbird-systemtray-connecting-dark.ico
    +var iconConnectingDark []byte
    +
    +//go:embed netbird-systemtray-error.ico
    +var iconError []byte
    +
    +//go:embed netbird-systemtray-error-dark.ico
    +var iconErrorDark []byte
    
    From 87311074f1a67f3f54ed0fce25c51cfeda11f7e6 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?C=C3=A9sar=20Gon=C3=A7alves?= 
    Date: Thu, 20 Feb 2025 09:56:22 +0000
    Subject: [PATCH 12/23] [misc] improvement(template): add traefik labels to
     relay (#3333)
    
    ---
     infrastructure_files/docker-compose.yml.tmpl.traefik | 4 ++++
     1 file changed, 4 insertions(+)
    
    diff --git a/infrastructure_files/docker-compose.yml.tmpl.traefik b/infrastructure_files/docker-compose.yml.tmpl.traefik
    index 71471c3ef..dcd3f955c 100644
    --- a/infrastructure_files/docker-compose.yml.tmpl.traefik
    +++ b/infrastructure_files/docker-compose.yml.tmpl.traefik
    @@ -67,6 +67,10 @@ services:
           options:
             max-size: "500m"
             max-file: "2"
    +    labels:
    +    - traefik.enable=true
    +    - traefik.http.routers.netbird-relay.rule=Host(`$NETBIRD_DOMAIN`) && PathPrefix(`/relay`)
    +    - traefik.http.services.netbird-relay.loadbalancer.server.port=$NETBIRD_RELAY_PORT
     
       # Management
       management:
    
    From 62a0c358f9ea9e1c69dd30a5328551f18e18f8a7 Mon Sep 17 00:00:00 2001
    From: Viktor Liu <17948409+lixmal@users.noreply.github.com>
    Date: Thu, 20 Feb 2025 11:00:02 +0100
    Subject: [PATCH 13/23] [client] Add UI client event notifications (#3207)
    
    ---
     client/cmd/status.go                   |   35 +-
     client/cmd/status_event.go             |   69 ++
     client/cmd/status_test.go              |   28 +-
     client/internal/config.go              |   14 +
     client/internal/dns/upstream.go        |    9 +
     client/internal/peer/status.go         |  125 ++
     client/internal/routemanager/client.go |   76 +-
     client/proto/daemon.pb.go              | 1496 ++++++++++++++++--------
     client/proto/daemon.proto              |   42 +
     client/proto/daemon_grpc.pb.go         |  102 +-
     client/server/event.go                 |   36 +
     client/server/server.go                |   29 +-
     client/ui/client_ui.go                 |   48 +-
     client/ui/event/event.go               |  151 +++
     14 files changed, 1685 insertions(+), 575 deletions(-)
     create mode 100644 client/cmd/status_event.go
     create mode 100644 client/server/event.go
     create mode 100644 client/ui/event/event.go
    
    diff --git a/client/cmd/status.go b/client/cmd/status.go
    index fa4bff77b..bf4588ce4 100644
    --- a/client/cmd/status.go
    +++ b/client/cmd/status.go
    @@ -39,7 +39,6 @@ type peerStateDetailOutput struct {
     	TransferSent           int64            `json:"transferSent" yaml:"transferSent"`
     	Latency                time.Duration    `json:"latency" yaml:"latency"`
     	RosenpassEnabled       bool             `json:"quantumResistance" yaml:"quantumResistance"`
    -	Routes                 []string         `json:"routes" yaml:"routes"`
     	Networks               []string         `json:"networks" yaml:"networks"`
     }
     
    @@ -98,9 +97,9 @@ type statusOutputOverview struct {
     	FQDN                string                     `json:"fqdn" yaml:"fqdn"`
     	RosenpassEnabled    bool                       `json:"quantumResistance" yaml:"quantumResistance"`
     	RosenpassPermissive bool                       `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"`
    -	Routes              []string                   `json:"routes" yaml:"routes"`
     	Networks            []string                   `json:"networks" yaml:"networks"`
     	NSServerGroups      []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"`
    +	Events              []systemEventOutput        `json:"events" yaml:"events"`
     }
     
     var (
    @@ -284,9 +283,9 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
     		FQDN:                pbFullStatus.GetLocalPeerState().GetFqdn(),
     		RosenpassEnabled:    pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(),
     		RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(),
    -		Routes:              pbFullStatus.GetLocalPeerState().GetNetworks(),
     		Networks:            pbFullStatus.GetLocalPeerState().GetNetworks(),
     		NSServerGroups:      mapNSGroups(pbFullStatus.GetDnsServers()),
    +		Events:              mapEvents(pbFullStatus.GetEvents()),
     	}
     
     	if anonymizeFlag {
    @@ -393,7 +392,6 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput {
     			TransferSent:           transferSent,
     			Latency:                pbPeerState.GetLatency().AsDuration(),
     			RosenpassEnabled:       pbPeerState.GetRosenpassEnabled(),
    -			Routes:                 pbPeerState.GetNetworks(),
     			Networks:               pbPeerState.GetNetworks(),
     		}
     
    @@ -559,7 +557,6 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
     			"NetBird IP: %s\n"+
     			"Interface type: %s\n"+
     			"Quantum resistance: %s\n"+
    -			"Routes: %s\n"+
     			"Networks: %s\n"+
     			"Peers count: %s\n",
     		fmt.Sprintf("%s/%s%s", goos, goarch, goarm),
    @@ -574,7 +571,6 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
     		interfaceTypeString,
     		rosenpassEnabledStatus,
     		networks,
    -		networks,
     		peersCountString,
     	)
     	return summary
    @@ -582,13 +578,17 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
     
     func parseToFullDetailSummary(overview statusOutputOverview) string {
     	parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive)
    +	parsedEventsString := parseEvents(overview.Events)
     	summary := parseGeneralSummary(overview, true, true, true)
     
     	return fmt.Sprintf(
     		"Peers detail:"+
    +			"%s\n"+
    +			"Events:"+
     			"%s\n"+
     			"%s",
     		parsedPeersString,
    +		parsedEventsString,
     		summary,
     	)
     }
    @@ -657,7 +657,6 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
     				"  Last WireGuard handshake: %s\n"+
     				"  Transfer status (received/sent) %s/%s\n"+
     				"  Quantum resistance: %s\n"+
    -				"  Routes: %s\n"+
     				"  Networks: %s\n"+
     				"  Latency: %s\n",
     			peerState.FQDN,
    @@ -676,7 +675,6 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
     			toIEC(peerState.TransferSent),
     			rosenpassEnabledStatus,
     			networks,
    -			networks,
     			peerState.Latency.String(),
     		)
     
    @@ -825,14 +823,6 @@ func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) {
     	for i, route := range peer.Networks {
     		peer.Networks[i] = a.AnonymizeRoute(route)
     	}
    -
    -	for i, route := range peer.Routes {
    -		peer.Routes[i] = a.AnonymizeIPString(route)
    -	}
    -
    -	for i, route := range peer.Routes {
    -		peer.Routes[i] = a.AnonymizeRoute(route)
    -	}
     }
     
     func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview) {
    @@ -870,9 +860,14 @@ func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview)
     		overview.Networks[i] = a.AnonymizeRoute(route)
     	}
     
    -	for i, route := range overview.Routes {
    -		overview.Routes[i] = a.AnonymizeRoute(route)
    -	}
    -
     	overview.FQDN = a.AnonymizeDomain(overview.FQDN)
    +
    +	for i, event := range overview.Events {
    +		overview.Events[i].Message = a.AnonymizeString(event.Message)
    +		overview.Events[i].UserMessage = a.AnonymizeString(event.UserMessage)
    +
    +		for k, v := range event.Metadata {
    +			event.Metadata[k] = a.AnonymizeString(v)
    +		}
    +	}
     }
    diff --git a/client/cmd/status_event.go b/client/cmd/status_event.go
    new file mode 100644
    index 000000000..9331570e6
    --- /dev/null
    +++ b/client/cmd/status_event.go
    @@ -0,0 +1,69 @@
    +package cmd
    +
    +import (
    +	"fmt"
    +	"sort"
    +	"strings"
    +	"time"
    +
    +	"github.com/netbirdio/netbird/client/proto"
    +)
    +
    +type systemEventOutput struct {
    +	ID          string            `json:"id" yaml:"id"`
    +	Severity    string            `json:"severity" yaml:"severity"`
    +	Category    string            `json:"category" yaml:"category"`
    +	Message     string            `json:"message" yaml:"message"`
    +	UserMessage string            `json:"userMessage" yaml:"userMessage"`
    +	Timestamp   time.Time         `json:"timestamp" yaml:"timestamp"`
    +	Metadata    map[string]string `json:"metadata" yaml:"metadata"`
    +}
    +
    +func mapEvents(protoEvents []*proto.SystemEvent) []systemEventOutput {
    +	events := make([]systemEventOutput, len(protoEvents))
    +	for i, event := range protoEvents {
    +		events[i] = systemEventOutput{
    +			ID:          event.GetId(),
    +			Severity:    event.GetSeverity().String(),
    +			Category:    event.GetCategory().String(),
    +			Message:     event.GetMessage(),
    +			UserMessage: event.GetUserMessage(),
    +			Timestamp:   event.GetTimestamp().AsTime(),
    +			Metadata:    event.GetMetadata(),
    +		}
    +	}
    +	return events
    +}
    +
    +func parseEvents(events []systemEventOutput) string {
    +	if len(events) == 0 {
    +		return " No events recorded"
    +	}
    +
    +	var eventsString strings.Builder
    +	for _, event := range events {
    +		timeStr := timeAgo(event.Timestamp)
    +
    +		metadataStr := ""
    +		if len(event.Metadata) > 0 {
    +			pairs := make([]string, 0, len(event.Metadata))
    +			for k, v := range event.Metadata {
    +				pairs = append(pairs, fmt.Sprintf("%s: %s", k, v))
    +			}
    +			sort.Strings(pairs)
    +			metadataStr = fmt.Sprintf("\n    Metadata: %s", strings.Join(pairs, ", "))
    +		}
    +
    +		eventsString.WriteString(fmt.Sprintf("\n  [%s] %s (%s)"+
    +			"\n    Message: %s"+
    +			"\n    Time: %s%s",
    +			event.Severity,
    +			event.Category,
    +			event.ID,
    +			event.Message,
    +			timeStr,
    +			metadataStr,
    +		))
    +	}
    +	return eventsString.String()
    +}
    diff --git a/client/cmd/status_test.go b/client/cmd/status_test.go
    index 1f1e95726..1e240d192 100644
    --- a/client/cmd/status_test.go
    +++ b/client/cmd/status_test.go
    @@ -146,9 +146,6 @@ var overview = statusOutputOverview{
     				LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC),
     				TransferReceived:       200,
     				TransferSent:           100,
    -				Routes: []string{
    -					"10.1.0.0/24",
    -				},
     				Networks: []string{
     					"10.1.0.0/24",
     				},
    @@ -176,6 +173,7 @@ var overview = statusOutputOverview{
     			},
     		},
     	},
    +	Events:        []systemEventOutput{},
     	CliVersion:    version.NetbirdVersion(),
     	DaemonVersion: "0.14.1",
     	ManagementState: managementStateOutput{
    @@ -230,9 +228,6 @@ var overview = statusOutputOverview{
     			Error:   "timeout",
     		},
     	},
    -	Routes: []string{
    -		"10.10.0.0/24",
    -	},
     	Networks: []string{
     		"10.10.0.0/24",
     	},
    @@ -299,9 +294,6 @@ func TestParsingToJSON(t *testing.T) {
                     "transferSent": 100,
     				"latency": 10000000,
                     "quantumResistance": false,
    -                "routes": [
    -                  "10.1.0.0/24"
    -                ],
                     "networks": [
                       "10.1.0.0/24"
                     ]
    @@ -327,7 +319,6 @@ func TestParsingToJSON(t *testing.T) {
                     "transferSent": 1000,
     				"latency": 10000000,
                     "quantumResistance": false,
    -                "routes": null,
                     "networks": null
                   }
                 ]
    @@ -366,9 +357,6 @@ func TestParsingToJSON(t *testing.T) {
               "fqdn": "some-localhost.awesome-domain.com",
               "quantumResistance": false,
               "quantumResistancePermissive": false,
    -          "routes": [
    -            "10.10.0.0/24"
    -          ],
               "networks": [
                 "10.10.0.0/24"
               ],
    @@ -393,7 +381,8 @@ func TestParsingToJSON(t *testing.T) {
                   "enabled": false,
                   "error": "timeout"
                 }
    -          ]
    +          ],
    +          "events": []
             }`
     	// @formatter:on
     
    @@ -429,8 +418,6 @@ func TestParsingToYAML(t *testing.T) {
               transferSent: 100
               latency: 10ms
               quantumResistance: false
    -          routes:
    -            - 10.1.0.0/24
               networks:
                 - 10.1.0.0/24
             - fqdn: peer-2.awesome-domain.com
    @@ -451,7 +438,6 @@ func TestParsingToYAML(t *testing.T) {
               transferSent: 1000
               latency: 10ms
               quantumResistance: false
    -          routes: []
               networks: []
     cliVersion: development
     daemonVersion: 0.14.1
    @@ -479,8 +465,6 @@ usesKernelInterface: true
     fqdn: some-localhost.awesome-domain.com
     quantumResistance: false
     quantumResistancePermissive: false
    -routes:
    -    - 10.10.0.0/24
     networks:
         - 10.10.0.0/24
     dnsServers:
    @@ -497,6 +481,7 @@ dnsServers:
             - example.net
           enabled: false
           error: timeout
    +events: []
     `
     
     	assert.Equal(t, expectedYAML, yaml)
    @@ -526,7 +511,6 @@ func TestParsingToDetail(t *testing.T) {
       Last WireGuard handshake: %s
       Transfer status (received/sent) 200 B/100 B
       Quantum resistance: false
    -  Routes: 10.1.0.0/24
       Networks: 10.1.0.0/24
       Latency: 10ms
     
    @@ -543,10 +527,10 @@ func TestParsingToDetail(t *testing.T) {
       Last WireGuard handshake: %s
       Transfer status (received/sent) 2.0 KiB/1000 B
       Quantum resistance: false
    -  Routes: -
       Networks: -
       Latency: 10ms
     
    +Events: No events recorded
     OS: %s/%s
     Daemon version: 0.14.1
     CLI version: %s
    @@ -562,7 +546,6 @@ FQDN: some-localhost.awesome-domain.com
     NetBird IP: 192.168.178.100/16
     Interface type: Kernel
     Quantum resistance: false
    -Routes: 10.10.0.0/24
     Networks: 10.10.0.0/24
     Peers count: 2/2 Connected
     `, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion)
    @@ -584,7 +567,6 @@ FQDN: some-localhost.awesome-domain.com
     NetBird IP: 192.168.178.100/16
     Interface type: Kernel
     Quantum resistance: false
    -Routes: 10.10.0.0/24
     Networks: 10.10.0.0/24
     Peers count: 2/2 Connected
     `
    diff --git a/client/internal/config.go b/client/internal/config.go
    index 3196c4e04..5703539cc 100644
    --- a/client/internal/config.go
    +++ b/client/internal/config.go
    @@ -68,6 +68,8 @@ type ConfigInput struct {
     	DisableFirewall     *bool
     
     	BlockLANAccess *bool
    +
    +	DisableNotifications *bool
     }
     
     // Config Configuration type
    @@ -93,6 +95,8 @@ type Config struct {
     
     	BlockLANAccess bool
     
    +	DisableNotifications bool
    +
     	// SSHKey is a private SSH key in a PEM format
     	SSHKey string
     
    @@ -469,6 +473,16 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
     		updated = true
     	}
     
    +	if input.DisableNotifications != nil && *input.DisableNotifications != config.DisableNotifications {
    +		if *input.DisableNotifications {
    +			log.Infof("disabling notifications")
    +		} else {
    +			log.Infof("enabling notifications")
    +		}
    +		config.DisableNotifications = *input.DisableNotifications
    +		updated = true
    +	}
    +
     	if input.ClientCertKeyPath != "" {
     		config.ClientCertKeyPath = input.ClientCertKeyPath
     		updated = true
    diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go
    index 4c69a173d..d269107e3 100644
    --- a/client/internal/dns/upstream.go
    +++ b/client/internal/dns/upstream.go
    @@ -19,6 +19,7 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	"github.com/netbirdio/netbird/client/internal/peer"
    +	"github.com/netbirdio/netbird/client/proto"
     )
     
     const (
    @@ -230,6 +231,14 @@ func (u *upstreamResolverBase) probeAvailability() {
     	// didn't find a working upstream server, let's disable and try later
     	if !success {
     		u.disable(errors.ErrorOrNil())
    +
    +		u.statusRecorder.PublishEvent(
    +			proto.SystemEvent_WARNING,
    +			proto.SystemEvent_DNS,
    +			"All upstream servers failed",
    +			"Unable to reach one or more DNS servers. This might affect your ability to connect to some services.",
    +			map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")},
    +		)
     	}
     }
     
    diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go
    index 311ddbd7f..e9976270c 100644
    --- a/client/internal/peer/status.go
    +++ b/client/internal/peer/status.go
    @@ -7,21 +7,31 @@ import (
     	"sync"
     	"time"
     
    +	"github.com/google/uuid"
    +	log "github.com/sirupsen/logrus"
     	"golang.org/x/exp/maps"
     	"google.golang.org/grpc/codes"
     	gstatus "google.golang.org/grpc/status"
    +	"google.golang.org/protobuf/types/known/timestamppb"
     
     	"github.com/netbirdio/netbird/client/iface/configurer"
     	"github.com/netbirdio/netbird/client/internal/relay"
    +	"github.com/netbirdio/netbird/client/proto"
     	"github.com/netbirdio/netbird/management/domain"
     	relayClient "github.com/netbirdio/netbird/relay/client"
     )
     
    +const eventQueueSize = 10
    +
     type ResolvedDomainInfo struct {
     	Prefixes     []netip.Prefix
     	ParentDomain domain.Domain
     }
     
    +type EventListener interface {
    +	OnEvent(event *proto.SystemEvent)
    +}
    +
     // State contains the latest state of a peer
     type State struct {
     	Mux                        *sync.RWMutex
    @@ -157,6 +167,10 @@ type Status struct {
     	peerListChangedForNotification bool
     
     	relayMgr *relayClient.Manager
    +
    +	eventMux     sync.RWMutex
    +	eventStreams map[string]chan *proto.SystemEvent
    +	eventQueue   *EventQueue
     }
     
     // NewRecorder returns a new Status instance
    @@ -164,6 +178,8 @@ func NewRecorder(mgmAddress string) *Status {
     	return &Status{
     		peers:                 make(map[string]State),
     		changeNotify:          make(map[string]chan struct{}),
    +		eventStreams:          make(map[string]chan *proto.SystemEvent),
    +		eventQueue:            NewEventQueue(eventQueueSize),
     		offlinePeers:          make([]State, 0),
     		notifier:              newNotifier(),
     		mgmAddress:            mgmAddress,
    @@ -806,3 +822,112 @@ func (d *Status) notifyAddressChanged() {
     func (d *Status) numOfPeers() int {
     	return len(d.peers) + len(d.offlinePeers)
     }
    +
    +// PublishEvent adds an event to the queue and distributes it to all subscribers
    +func (d *Status) PublishEvent(
    +	severity proto.SystemEvent_Severity,
    +	category proto.SystemEvent_Category,
    +	msg string,
    +	userMsg string,
    +	metadata map[string]string,
    +) {
    +	event := &proto.SystemEvent{
    +		Id:          uuid.New().String(),
    +		Severity:    severity,
    +		Category:    category,
    +		Message:     msg,
    +		UserMessage: userMsg,
    +		Metadata:    metadata,
    +		Timestamp:   timestamppb.Now(),
    +	}
    +
    +	d.eventMux.Lock()
    +	defer d.eventMux.Unlock()
    +
    +	d.eventQueue.Add(event)
    +
    +	for _, stream := range d.eventStreams {
    +		select {
    +		case stream <- event:
    +		default:
    +			log.Debugf("event stream buffer full, skipping event: %v", event)
    +		}
    +	}
    +
    +	log.Debugf("event published: %v", event)
    +}
    +
    +// SubscribeToEvents returns a new event subscription
    +func (d *Status) SubscribeToEvents() *EventSubscription {
    +	d.eventMux.Lock()
    +	defer d.eventMux.Unlock()
    +
    +	id := uuid.New().String()
    +	stream := make(chan *proto.SystemEvent, 10)
    +	d.eventStreams[id] = stream
    +
    +	return &EventSubscription{
    +		id:     id,
    +		events: stream,
    +	}
    +}
    +
    +// UnsubscribeFromEvents removes an event subscription
    +func (d *Status) UnsubscribeFromEvents(sub *EventSubscription) {
    +	if sub == nil {
    +		return
    +	}
    +
    +	d.eventMux.Lock()
    +	defer d.eventMux.Unlock()
    +
    +	if stream, exists := d.eventStreams[sub.id]; exists {
    +		close(stream)
    +		delete(d.eventStreams, sub.id)
    +	}
    +}
    +
    +// GetEventHistory returns all events in the queue
    +func (d *Status) GetEventHistory() []*proto.SystemEvent {
    +	return d.eventQueue.GetAll()
    +}
    +
    +type EventQueue struct {
    +	maxSize int
    +	events  []*proto.SystemEvent
    +	mutex   sync.RWMutex
    +}
    +
    +func NewEventQueue(size int) *EventQueue {
    +	return &EventQueue{
    +		maxSize: size,
    +		events:  make([]*proto.SystemEvent, 0, size),
    +	}
    +}
    +
    +func (q *EventQueue) Add(event *proto.SystemEvent) {
    +	q.mutex.Lock()
    +	defer q.mutex.Unlock()
    +
    +	q.events = append(q.events, event)
    +
    +	if len(q.events) > q.maxSize {
    +		q.events = q.events[len(q.events)-q.maxSize:]
    +	}
    +}
    +
    +func (q *EventQueue) GetAll() []*proto.SystemEvent {
    +	q.mutex.RLock()
    +	defer q.mutex.RUnlock()
    +
    +	return slices.Clone(q.events)
    +}
    +
    +type EventSubscription struct {
    +	id     string
    +	events chan *proto.SystemEvent
    +}
    +
    +func (s *EventSubscription) Events() <-chan *proto.SystemEvent {
    +	return s.events
    +}
    diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go
    index faf0fadaa..3238dd831 100644
    --- a/client/internal/routemanager/client.go
    +++ b/client/internal/routemanager/client.go
    @@ -19,6 +19,7 @@ import (
     	"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     	"github.com/netbirdio/netbird/client/internal/routemanager/static"
    +	"github.com/netbirdio/netbird/client/proto"
     	"github.com/netbirdio/netbird/route"
     )
     
    @@ -28,6 +29,15 @@ const (
     	handlerTypeStatic
     )
     
    +type reason int
    +
    +const (
    +	reasonUnknown reason = iota
    +	reasonRouteUpdate
    +	reasonPeerUpdate
    +	reasonShutdown
    +)
    +
     type routerPeerStatus struct {
     	connected bool
     	relayed   bool
    @@ -255,7 +265,7 @@ func (c *clientNetwork) removeRouteFromWireGuardPeer() error {
     	return nil
     }
     
    -func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
    +func (c *clientNetwork) removeRouteFromPeerAndSystem(rsn reason) error {
     	if c.currentChosen == nil {
     		return nil
     	}
    @@ -269,17 +279,19 @@ func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
     		merr = multierror.Append(merr, fmt.Errorf("remove route: %w", err))
     	}
     
    +	c.disconnectEvent(rsn)
    +
     	return nberrors.FormatErrorOrNil(merr)
     }
     
    -func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
    +func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error {
     	routerPeerStatuses := c.getRouterPeerStatuses()
     
     	newChosenID := c.getBestRouteFromStatuses(routerPeerStatuses)
     
     	// If no route is chosen, remove the route from the peer and system
     	if newChosenID == "" {
    -		if err := c.removeRouteFromPeerAndSystem(); err != nil {
    +		if err := c.removeRouteFromPeerAndSystem(rsn); err != nil {
     			return fmt.Errorf("remove route for peer %s: %w", c.currentChosen.Peer, err)
     		}
     
    @@ -319,6 +331,58 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
     	return nil
     }
     
    +func (c *clientNetwork) disconnectEvent(rsn reason) {
    +	var defaultRoute bool
    +	for _, r := range c.routes {
    +		if r.Network.Bits() == 0 {
    +			defaultRoute = true
    +			break
    +		}
    +	}
    +
    +	if !defaultRoute {
    +		return
    +	}
    +
    +	var severity proto.SystemEvent_Severity
    +	var message string
    +	var userMessage string
    +	meta := make(map[string]string)
    +
    +	switch rsn {
    +	case reasonShutdown:
    +		severity = proto.SystemEvent_INFO
    +		message = "Default route removed"
    +		userMessage = "Exit node disconnected."
    +		meta["network"] = c.handler.String()
    +	case reasonRouteUpdate:
    +		severity = proto.SystemEvent_INFO
    +		message = "Default route updated due to configuration change"
    +		meta["network"] = c.handler.String()
    +	case reasonPeerUpdate:
    +		severity = proto.SystemEvent_WARNING
    +		message = "Default route disconnected due to peer unreachability"
    +		userMessage = "Exit node connection lost. Your internet access might be affected."
    +		if c.currentChosen != nil {
    +			meta["peer"] = c.currentChosen.Peer
    +			meta["network"] = c.handler.String()
    +		}
    +	default:
    +		severity = proto.SystemEvent_ERROR
    +		message = "Default route disconnected for unknown reason"
    +		userMessage = "Exit node disconnected for unknown reasons."
    +		meta["network"] = c.handler.String()
    +	}
    +
    +	c.statusRecorder.PublishEvent(
    +		severity,
    +		proto.SystemEvent_NETWORK,
    +		message,
    +		userMessage,
    +		meta,
    +	)
    +}
    +
     func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) {
     	go func() {
     		c.routeUpdate <- update
    @@ -361,12 +425,12 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() {
     		select {
     		case <-c.ctx.Done():
     			log.Debugf("Stopping watcher for network [%v]", c.handler)
    -			if err := c.removeRouteFromPeerAndSystem(); err != nil {
    +			if err := c.removeRouteFromPeerAndSystem(reasonShutdown); err != nil {
     				log.Errorf("Failed to remove routes for [%v]: %v", c.handler, err)
     			}
     			return
     		case <-c.peerStateUpdate:
    -			err := c.recalculateRouteAndUpdatePeerAndSystem()
    +			err := c.recalculateRouteAndUpdatePeerAndSystem(reasonPeerUpdate)
     			if err != nil {
     				log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err)
     			}
    @@ -385,7 +449,7 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() {
     
     			if isTrueRouteUpdate {
     				log.Debug("Client network update contains different routes, recalculating routes")
    -				err := c.recalculateRouteAndUpdatePeerAndSystem()
    +				err := c.recalculateRouteAndUpdatePeerAndSystem(reasonRouteUpdate)
     				if err != nil {
     					log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err)
     				}
    diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go
    index c9651efed..b40f6beea 100644
    --- a/client/proto/daemon.pb.go
    +++ b/client/proto/daemon.pb.go
    @@ -87,6 +87,110 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) {
     	return file_daemon_proto_rawDescGZIP(), []int{0}
     }
     
    +type SystemEvent_Severity int32
    +
    +const (
    +	SystemEvent_INFO     SystemEvent_Severity = 0
    +	SystemEvent_WARNING  SystemEvent_Severity = 1
    +	SystemEvent_ERROR    SystemEvent_Severity = 2
    +	SystemEvent_CRITICAL SystemEvent_Severity = 3
    +)
    +
    +// Enum value maps for SystemEvent_Severity.
    +var (
    +	SystemEvent_Severity_name = map[int32]string{
    +		0: "INFO",
    +		1: "WARNING",
    +		2: "ERROR",
    +		3: "CRITICAL",
    +	}
    +	SystemEvent_Severity_value = map[string]int32{
    +		"INFO":     0,
    +		"WARNING":  1,
    +		"ERROR":    2,
    +		"CRITICAL": 3,
    +	}
    +)
    +
    +func (x SystemEvent_Severity) Enum() *SystemEvent_Severity {
    +	p := new(SystemEvent_Severity)
    +	*p = x
    +	return p
    +}
    +
    +func (x SystemEvent_Severity) String() string {
    +	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
    +}
    +
    +func (SystemEvent_Severity) Descriptor() protoreflect.EnumDescriptor {
    +	return file_daemon_proto_enumTypes[1].Descriptor()
    +}
    +
    +func (SystemEvent_Severity) Type() protoreflect.EnumType {
    +	return &file_daemon_proto_enumTypes[1]
    +}
    +
    +func (x SystemEvent_Severity) Number() protoreflect.EnumNumber {
    +	return protoreflect.EnumNumber(x)
    +}
    +
    +// Deprecated: Use SystemEvent_Severity.Descriptor instead.
    +func (SystemEvent_Severity) EnumDescriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{45, 0}
    +}
    +
    +type SystemEvent_Category int32
    +
    +const (
    +	SystemEvent_NETWORK        SystemEvent_Category = 0
    +	SystemEvent_DNS            SystemEvent_Category = 1
    +	SystemEvent_AUTHENTICATION SystemEvent_Category = 2
    +	SystemEvent_CONNECTIVITY   SystemEvent_Category = 3
    +)
    +
    +// Enum value maps for SystemEvent_Category.
    +var (
    +	SystemEvent_Category_name = map[int32]string{
    +		0: "NETWORK",
    +		1: "DNS",
    +		2: "AUTHENTICATION",
    +		3: "CONNECTIVITY",
    +	}
    +	SystemEvent_Category_value = map[string]int32{
    +		"NETWORK":        0,
    +		"DNS":            1,
    +		"AUTHENTICATION": 2,
    +		"CONNECTIVITY":   3,
    +	}
    +)
    +
    +func (x SystemEvent_Category) Enum() *SystemEvent_Category {
    +	p := new(SystemEvent_Category)
    +	*p = x
    +	return p
    +}
    +
    +func (x SystemEvent_Category) String() string {
    +	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
    +}
    +
    +func (SystemEvent_Category) Descriptor() protoreflect.EnumDescriptor {
    +	return file_daemon_proto_enumTypes[2].Descriptor()
    +}
    +
    +func (SystemEvent_Category) Type() protoreflect.EnumType {
    +	return &file_daemon_proto_enumTypes[2]
    +}
    +
    +func (x SystemEvent_Category) Number() protoreflect.EnumNumber {
    +	return protoreflect.EnumNumber(x)
    +}
    +
    +// Deprecated: Use SystemEvent_Category.Descriptor instead.
    +func (SystemEvent_Category) EnumDescriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{45, 1}
    +}
    +
     type LoginRequest struct {
     	state         protoimpl.MessageState
     	sizeCache     protoimpl.SizeCache
    @@ -127,6 +231,7 @@ type LoginRequest struct {
     	DisableDns           *bool                `protobuf:"varint,22,opt,name=disable_dns,json=disableDns,proto3,oneof" json:"disable_dns,omitempty"`
     	DisableFirewall      *bool                `protobuf:"varint,23,opt,name=disable_firewall,json=disableFirewall,proto3,oneof" json:"disable_firewall,omitempty"`
     	BlockLanAccess       *bool                `protobuf:"varint,24,opt,name=block_lan_access,json=blockLanAccess,proto3,oneof" json:"block_lan_access,omitempty"`
    +	DisableNotifications *bool                `protobuf:"varint,25,opt,name=disable_notifications,json=disableNotifications,proto3,oneof" json:"disable_notifications,omitempty"`
     }
     
     func (x *LoginRequest) Reset() {
    @@ -330,6 +435,13 @@ func (x *LoginRequest) GetBlockLanAccess() bool {
     	return false
     }
     
    +func (x *LoginRequest) GetDisableNotifications() bool {
    +	if x != nil && x.DisableNotifications != nil {
    +		return *x.DisableNotifications
    +	}
    +	return false
    +}
    +
     type LoginResponse struct {
     	state         protoimpl.MessageState
     	sizeCache     protoimpl.SizeCache
    @@ -810,13 +922,14 @@ type GetConfigResponse struct {
     	// preSharedKey settings value.
     	PreSharedKey string `protobuf:"bytes,4,opt,name=preSharedKey,proto3" json:"preSharedKey,omitempty"`
     	// adminURL settings value.
    -	AdminURL            string `protobuf:"bytes,5,opt,name=adminURL,proto3" json:"adminURL,omitempty"`
    -	InterfaceName       string `protobuf:"bytes,6,opt,name=interfaceName,proto3" json:"interfaceName,omitempty"`
    -	WireguardPort       int64  `protobuf:"varint,7,opt,name=wireguardPort,proto3" json:"wireguardPort,omitempty"`
    -	DisableAutoConnect  bool   `protobuf:"varint,9,opt,name=disableAutoConnect,proto3" json:"disableAutoConnect,omitempty"`
    -	ServerSSHAllowed    bool   `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"`
    -	RosenpassEnabled    bool   `protobuf:"varint,11,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"`
    -	RosenpassPermissive bool   `protobuf:"varint,12,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"`
    +	AdminURL             string `protobuf:"bytes,5,opt,name=adminURL,proto3" json:"adminURL,omitempty"`
    +	InterfaceName        string `protobuf:"bytes,6,opt,name=interfaceName,proto3" json:"interfaceName,omitempty"`
    +	WireguardPort        int64  `protobuf:"varint,7,opt,name=wireguardPort,proto3" json:"wireguardPort,omitempty"`
    +	DisableAutoConnect   bool   `protobuf:"varint,9,opt,name=disableAutoConnect,proto3" json:"disableAutoConnect,omitempty"`
    +	ServerSSHAllowed     bool   `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"`
    +	RosenpassEnabled     bool   `protobuf:"varint,11,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"`
    +	RosenpassPermissive  bool   `protobuf:"varint,12,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"`
    +	DisableNotifications bool   `protobuf:"varint,13,opt,name=disable_notifications,json=disableNotifications,proto3" json:"disable_notifications,omitempty"`
     }
     
     func (x *GetConfigResponse) Reset() {
    @@ -928,6 +1041,13 @@ func (x *GetConfigResponse) GetRosenpassPermissive() bool {
     	return false
     }
     
    +func (x *GetConfigResponse) GetDisableNotifications() bool {
    +	if x != nil {
    +		return x.DisableNotifications
    +	}
    +	return false
    +}
    +
     // PeerState contains the latest state of a peer
     type PeerState struct {
     	state         protoimpl.MessageState
    @@ -1475,6 +1595,7 @@ type FullStatus struct {
     	Peers           []*PeerState     `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"`
     	Relays          []*RelayState    `protobuf:"bytes,5,rep,name=relays,proto3" json:"relays,omitempty"`
     	DnsServers      []*NSGroupState  `protobuf:"bytes,6,rep,name=dns_servers,json=dnsServers,proto3" json:"dns_servers,omitempty"`
    +	Events          []*SystemEvent   `protobuf:"bytes,7,rep,name=events,proto3" json:"events,omitempty"`
     }
     
     func (x *FullStatus) Reset() {
    @@ -1551,6 +1672,13 @@ func (x *FullStatus) GetDnsServers() []*NSGroupState {
     	return nil
     }
     
    +func (x *FullStatus) GetEvents() []*SystemEvent {
    +	if x != nil {
    +		return x.Events
    +	}
    +	return nil
    +}
    +
     type ListNetworksRequest struct {
     	state         protoimpl.MessageState
     	sizeCache     protoimpl.SizeCache
    @@ -2895,6 +3023,224 @@ func (x *TracePacketResponse) GetFinalDisposition() bool {
     	return false
     }
     
    +type SubscribeRequest struct {
    +	state         protoimpl.MessageState
    +	sizeCache     protoimpl.SizeCache
    +	unknownFields protoimpl.UnknownFields
    +}
    +
    +func (x *SubscribeRequest) Reset() {
    +	*x = SubscribeRequest{}
    +	if protoimpl.UnsafeEnabled {
    +		mi := &file_daemon_proto_msgTypes[44]
    +		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
    +		ms.StoreMessageInfo(mi)
    +	}
    +}
    +
    +func (x *SubscribeRequest) String() string {
    +	return protoimpl.X.MessageStringOf(x)
    +}
    +
    +func (*SubscribeRequest) ProtoMessage() {}
    +
    +func (x *SubscribeRequest) ProtoReflect() protoreflect.Message {
    +	mi := &file_daemon_proto_msgTypes[44]
    +	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 SubscribeRequest.ProtoReflect.Descriptor instead.
    +func (*SubscribeRequest) Descriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{44}
    +}
    +
    +type SystemEvent struct {
    +	state         protoimpl.MessageState
    +	sizeCache     protoimpl.SizeCache
    +	unknownFields protoimpl.UnknownFields
    +
    +	Id          string                 `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
    +	Severity    SystemEvent_Severity   `protobuf:"varint,2,opt,name=severity,proto3,enum=daemon.SystemEvent_Severity" json:"severity,omitempty"`
    +	Category    SystemEvent_Category   `protobuf:"varint,3,opt,name=category,proto3,enum=daemon.SystemEvent_Category" json:"category,omitempty"`
    +	Message     string                 `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"`
    +	UserMessage string                 `protobuf:"bytes,5,opt,name=userMessage,proto3" json:"userMessage,omitempty"`
    +	Timestamp   *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
    +	Metadata    map[string]string      `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
    +}
    +
    +func (x *SystemEvent) Reset() {
    +	*x = SystemEvent{}
    +	if protoimpl.UnsafeEnabled {
    +		mi := &file_daemon_proto_msgTypes[45]
    +		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
    +		ms.StoreMessageInfo(mi)
    +	}
    +}
    +
    +func (x *SystemEvent) String() string {
    +	return protoimpl.X.MessageStringOf(x)
    +}
    +
    +func (*SystemEvent) ProtoMessage() {}
    +
    +func (x *SystemEvent) ProtoReflect() protoreflect.Message {
    +	mi := &file_daemon_proto_msgTypes[45]
    +	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 SystemEvent.ProtoReflect.Descriptor instead.
    +func (*SystemEvent) Descriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{45}
    +}
    +
    +func (x *SystemEvent) GetId() string {
    +	if x != nil {
    +		return x.Id
    +	}
    +	return ""
    +}
    +
    +func (x *SystemEvent) GetSeverity() SystemEvent_Severity {
    +	if x != nil {
    +		return x.Severity
    +	}
    +	return SystemEvent_INFO
    +}
    +
    +func (x *SystemEvent) GetCategory() SystemEvent_Category {
    +	if x != nil {
    +		return x.Category
    +	}
    +	return SystemEvent_NETWORK
    +}
    +
    +func (x *SystemEvent) GetMessage() string {
    +	if x != nil {
    +		return x.Message
    +	}
    +	return ""
    +}
    +
    +func (x *SystemEvent) GetUserMessage() string {
    +	if x != nil {
    +		return x.UserMessage
    +	}
    +	return ""
    +}
    +
    +func (x *SystemEvent) GetTimestamp() *timestamppb.Timestamp {
    +	if x != nil {
    +		return x.Timestamp
    +	}
    +	return nil
    +}
    +
    +func (x *SystemEvent) GetMetadata() map[string]string {
    +	if x != nil {
    +		return x.Metadata
    +	}
    +	return nil
    +}
    +
    +type GetEventsRequest struct {
    +	state         protoimpl.MessageState
    +	sizeCache     protoimpl.SizeCache
    +	unknownFields protoimpl.UnknownFields
    +}
    +
    +func (x *GetEventsRequest) Reset() {
    +	*x = GetEventsRequest{}
    +	if protoimpl.UnsafeEnabled {
    +		mi := &file_daemon_proto_msgTypes[46]
    +		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
    +		ms.StoreMessageInfo(mi)
    +	}
    +}
    +
    +func (x *GetEventsRequest) String() string {
    +	return protoimpl.X.MessageStringOf(x)
    +}
    +
    +func (*GetEventsRequest) ProtoMessage() {}
    +
    +func (x *GetEventsRequest) ProtoReflect() protoreflect.Message {
    +	mi := &file_daemon_proto_msgTypes[46]
    +	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 GetEventsRequest.ProtoReflect.Descriptor instead.
    +func (*GetEventsRequest) Descriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{46}
    +}
    +
    +type GetEventsResponse struct {
    +	state         protoimpl.MessageState
    +	sizeCache     protoimpl.SizeCache
    +	unknownFields protoimpl.UnknownFields
    +
    +	Events []*SystemEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"`
    +}
    +
    +func (x *GetEventsResponse) Reset() {
    +	*x = GetEventsResponse{}
    +	if protoimpl.UnsafeEnabled {
    +		mi := &file_daemon_proto_msgTypes[47]
    +		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
    +		ms.StoreMessageInfo(mi)
    +	}
    +}
    +
    +func (x *GetEventsResponse) String() string {
    +	return protoimpl.X.MessageStringOf(x)
    +}
    +
    +func (*GetEventsResponse) ProtoMessage() {}
    +
    +func (x *GetEventsResponse) ProtoReflect() protoreflect.Message {
    +	mi := &file_daemon_proto_msgTypes[47]
    +	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 GetEventsResponse.ProtoReflect.Descriptor instead.
    +func (*GetEventsResponse) Descriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{47}
    +}
    +
    +func (x *GetEventsResponse) GetEvents() []*SystemEvent {
    +	if x != nil {
    +		return x.Events
    +	}
    +	return nil
    +}
    +
     var File_daemon_proto protoreflect.FileDescriptor
     
     var file_daemon_proto_rawDesc = []byte{
    @@ -2905,7 +3251,7 @@ var file_daemon_proto_rawDesc = []byte{
     	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
     	0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
     	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74,
    -	0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x95, 0x0b, 0x0a, 0x0c, 0x4c, 0x6f,
    +	0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe9, 0x0b, 0x0a, 0x0c, 0x4c, 0x6f,
     	0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65,
     	0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65,
     	0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61,
    @@ -2976,409 +3322,468 @@ var file_daemon_proto_rawDesc = []byte{
     	0x61, 0x6c, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f,
     	0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08,
     	0x48, 0x0d, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65,
    -	0x73, 0x73, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70,
    -	0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69,
    -	0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e,
    -	0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17,
    -	0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68,
    -	0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61,
    -	0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13,
    -	0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f,
    -	0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
    -	0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f,
    -	0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13,
    -	0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72,
    -	0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f,
    -	0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a,
    -	0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
    -	0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61,
    -	0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61,
    -	0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11,
    -	0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73,
    -	0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c,
    -	0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64,
    -	0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65,
    -	0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65,
    -	0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63,
    -	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f,
    -	0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12,
    -	0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55,
    -	0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52,
    -	0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69,
    -	0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
    -	0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08,
    -	0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
    -	0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74,
    -	0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    -	0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a,
    -	0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53,
    -	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11,
    -	0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75,
    -	0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c,
    -	0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53,
    -	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a,
    -	0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73,
    -	0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61,
    -	0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66,
    -	0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65,
    -	0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22,
    -	0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e,
    -	0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12,
    -	0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x22, 0xb9, 0x03, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
    -	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61,
    -	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e,
    -	0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18,
    -	0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53,
    -	0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
    -	0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08,
    -	0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
    -	0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65,
    -	0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24,
    -	0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18,
    -	0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64,
    -	0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41,
    -	0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08,
    -	0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e,
    -	0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53,
    -	0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10,
    -	0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64,
    -	0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
    -	0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65,
    -	0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13,
    -	0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
    -	0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    -	0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x22, 0xde,
    -	0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02,
    -	0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06,
    -	0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75,
    -	0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74,
    -	0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74,
    -	0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74,
    -	0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 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, 0x10, 0x63, 0x6f, 0x6e, 0x6e,
    -	0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07,
    -	0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72,
    -	0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49,
    +	0x73, 0x73, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
    +	0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x19,
    +	0x20, 0x01, 0x28, 0x08, 0x48, 0x0e, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e,
    +	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x42,
    +	0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
    +	0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61,
    +	0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67,
    +	0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74,
    +	0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65,
    +	0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74,
    +	0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72,
    +	0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a,
    +	0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69,
    +	0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73,
    +	0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a,
    +	0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74,
    +	0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61,
    +	0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65,
    +	0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e,
    +	0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69,
    +	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
    +	0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f,
    +	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
    +	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
    +	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73,
    +	0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d,
    +	0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a,
    +	0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72,
    +	0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    +	0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
    +	0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
    +	0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a,
    +	0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71,
    +	0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    +	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    +	0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14,
    +	0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    +	0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
    +	0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
    +	0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74,
    +	0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82,
    +	0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    +	0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c,
    +	0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75,
    +	0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a,
    +	0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73,
    +	0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
    +	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xee, 0x03, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f,
    +	0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d,
    +	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55,
    +	0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69,
    +	0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c,
    +	0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79,
    +	0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d,
    +	0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61,
    +	0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50,
    +	0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67,
    +	0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61,
    +	0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74,
    +	0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76,
    +	0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c,
    +	0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
    +	0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10,
    +	0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
    +	0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72,
    +	0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72,
    +	0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69,
    +	0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f,
    +	0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28,
    +	0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69,
    +	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72,
    +	0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a,
    +	0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a,
    +	0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74,
    +	0x65, 0x18, 0x04, 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, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55,
    +	0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64,
    +	0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12,
    +	0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69,
    +	0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15,
    +	0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74,
    +	0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49,
     	0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18,
    -	0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43,
    -	0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16,
    -	0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61,
    -	0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65,
    -	0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65,
    -	0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61,
    -	0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64,
    -	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63,
    -	0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e,
    -	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65,
    -	0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70,
    -	0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f,
    -	0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e,
    -	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69,
    -	0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65,
    -	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, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
    -	0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72,
    -	0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79,
    -	0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74,
    -	0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18,
    -	0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a,
    -	0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c,
    -	0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70,
    -	0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65,
    -	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65,
    -	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 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, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72,
    -	0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22,
    -	0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
    -	0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65,
    -	0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20,
    -	0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72,
    -	0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65,
    -	0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
    -	0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
    -	0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28,
    -	0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d,
    -	0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
    -	0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64,
    -	0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65,
    -	0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67,
    -	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
    +	0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65,
    +	0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a,
    +	0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
    +	0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e,
    +	0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61,
    +	0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    +	0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64,
    +	0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61,
    +	0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    +	0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64,
    +	0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 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, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73,
    +	0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68,
    +	0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d,
    +	0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a,
    +	0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07,
    +	0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    +	0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28,
    +	0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62,
    +	0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18,
    +	0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12,
    +	0x33, 0x0a, 0x07, 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, 0x07, 0x6c, 0x61, 0x74,
    +	0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64,
    +	0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61,
    +	0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63,
    +	0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49,
    +	0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70,
    +	0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62,
    +	0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74,
    +	0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65,
    +	0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a,
    +	0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
    +	0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e,
    +	0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73,
    +	0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a,
    +	0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73,
    +	0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65,
    +	0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12,
    +	0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
    +	0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53,
    +	0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
     	0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09,
     	0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
     	0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72,
     	0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
    -	0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10,
    -	0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49,
    -	0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20,
    -	0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14,
    -	0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65,
    -	0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53,
    -	0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
    -	0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18,
    -	0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52,
    -	0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62,
    -	0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
    -	0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xd2, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c,
    -	0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    -	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
    -	0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    -	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69,
    -	0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
    -	0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53,
    -	0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
    -	0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65,
    -	0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65,
    -	0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06,
    -	0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65,
    -	0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x22, 0x15, 0x0a,
    -	0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06,
    -	0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72,
    -	0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e,
    -	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e,
    -	0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03,
    -	0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16,
    -	0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06,
    -	0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20,
    -	0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65,
    -	0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03,
    -	0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9,
    -	0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44,
    -	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61,
    -	0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65,
    -	0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07,
    -	0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64,
    -	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76,
    -	0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73,
    -	0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72,
    -	0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65,
    -	0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
    -	0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
    -	0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
    -	0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52,
    -	0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65,
    -	0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
    -	0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16,
    -	0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
    -	0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
    -	0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74,
    -	0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42,
    -	0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a,
    -	0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74,
    -	0x68, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
    -	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f,
    -	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
    -	0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e,
    -	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52,
    -	0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67,
    -	0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05,
    -	0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c,
    -	0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65,
    -	0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53,
    -	0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74,
    -	0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a,
    -	0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20,
    -	0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c,
    -	0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
    -	0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10,
    -	0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c,
    -	0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65,
    -	0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d,
    -	0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a,
    -	0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75,
    -	0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d,
    -	0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61,
    -	0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
    -	0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64,
    -	0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
    -	0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
    -	0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22,
    -	0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70,
    -	0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12,
    -	0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79,
    -	0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03,
    -	0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
    -	0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67,
    -	0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12,
    -	0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18,
    -	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12,
    -	0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
    -	0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
    -	0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
    -	0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
    -	0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72,
    -	0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50,
    -	0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69,
    -	0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64,
    -	0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c,
    -	0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09,
    -	0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32,
    -	0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67,
    -	0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01,
    -	0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20,
    -	0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88,
    -	0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18,
    -	0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64,
    -	0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61,
    -	0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65,
    -	0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f,
    -	0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a,
    -	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
    -	0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61,
    -	0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c,
    -	0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64,
    -	0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28,
    -	0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44,
    -	0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f,
    -	0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73,
    -	0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52,
    -	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65,
    -	0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    -	0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61,
    -	0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73,
    -	0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10,
    -	0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e,
    -	0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07,
    -	0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e,
    -	0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12,
    -	0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41,
    -	0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09,
    -	0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41,
    -	0x43, 0x45, 0x10, 0x07, 0x32, 0xdd, 0x09, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53,
    -	0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12,
    -	0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c,
    -	0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b,
    -	0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c,
    -	0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69,
    -	0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55,
    -	0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70,
    -	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74,
    -	0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74,
    -	0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e,
    -	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e,
    -	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65,
    -	0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    -	0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    -	0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f,
    -	0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b,
    -	0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
    -	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53,
    -	0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e,
    -	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74,
    -	0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53,
    -	0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65,
    -	0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    -	0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63,
    -	0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    -	0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64,
    -	0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75,
    -	0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e,
    -	0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a,
    -	0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65,
    -	0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73,
    -	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f,
    -	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
    +	0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63,
    +	0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c,
    +	0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61,
    +	0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76,
    +	0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
    +	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a,
    +	0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a,
    +	0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
    +	0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69,
    +	0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
    +	0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
    +	0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
    +	0x72, 0x22, 0xff, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
    +	0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73,
    +	0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f,
    +	0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61,
    +	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61,
    +	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65,
    +	0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65,
    +	0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20,
    +	0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c,
    +	0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12,
    +	0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06,
    +	0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53,
    +	0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53,
    +	0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73,
    +	0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65,
    +	0x6e, 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    +	0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69,
    +	0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
    +	0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53,
    +	0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71,
    +	0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49,
    +	0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03,
    +	0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18,
    +	0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73,
    +	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69,
    +	0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
    +	0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
    +	0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44,
    +	0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
    +	0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
    +	0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20,
    +	0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b,
    +	0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28,
    +	0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    +	0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e,
    +	0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73,
    +	0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45,
    +	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49,
    +	0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
    +	0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52,
    +	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d,
    +	0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79,
    +	0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a,
    +	0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
    +	0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13,
    +	0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f,
    +	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a,
    +	0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20,
    +	0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67,
    +	0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12,
     	0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c,
    +	0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65,
    +	0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65,
    +	0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    +	0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
    +	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13,
    +	0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
    +	0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65,
    +	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61,
    +	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73,
    +	0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65,
    +	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e,
    +	0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65,
    +	0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28,
    +	0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53,
    +	0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e,
    +	0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01,
    +	0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61,
    +	0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
    +	0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65,
    +	0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    +	0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61,
    +	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74,
    +	0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e,
    +	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74,
    +	0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65,
    +	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e,
    +	0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63,
    +	0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50,
    +	0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e,
    +	0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72,
    +	0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a,
    +	0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12,
    +	0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72,
    +	0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65,
    +	0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72,
    +	0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75,
    +	0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
    +	0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64,
    +	0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08,
    +	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
    +	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72,
    +	0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73,
    +	0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73,
    +	0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20,
    +	0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    +	0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f,
    +	0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69,
    +	0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18,
    +	0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54,
    +	0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c,
    +	0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74,
    +	0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d,
    +	0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70,
    +	0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69,
    +	0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74,
    +	0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d,
    +	0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f,
    +	0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74,
    +	0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
    +	0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
    +	0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66,
    +	0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c,
    +	0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61,
    +	0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42,
    +	0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64,
    +	0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50,
    +	0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
    +	0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67,
    +	0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e,
    +	0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f,
    +	0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72,
    +	0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x87, 0x04, 0x0a, 0x0b, 0x53,
    +	0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
    +	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65,
    +	0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e,
    +	0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65,
    +	0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79,
    +	0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65,
    +	0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18,
    +	0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72,
    +	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75,
    +	0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69,
    +	0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 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, 0x74, 0x69, 0x6d, 0x65, 0x73,
    +	0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
    +	0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61,
    +	0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
    +	0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45,
    +	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 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, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04,
    +	0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e,
    +	0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c,
    +	0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x08,
    +	0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57,
    +	0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12,
    +	0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e,
    +	0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49,
    +	0x54, 0x59, 0x10, 0x03, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74,
    +	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45,
    +	0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a,
    +	0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65,
    +	0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f,
    +	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57,
    +	0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09,
    +	0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52,
    +	0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08,
    +	0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55,
    +	0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xe7,
    +	0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
    +	0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    +	0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    +	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74,
    +	0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    +	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57,
    +	0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12,
    +	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15,
    +	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
    +	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    +	0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    +	0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f,
    +	0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65,
    +	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74,
    +	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65,
    +	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c,
    +	0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e,
    +	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    +	0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52,
    +	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65,
    +	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65,
    +	0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    +	0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a,
    +	0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c,
    +	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73,
    +	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f,
    +	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c,
     	0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
    -	0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12,
    -	0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65,
    -	0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61,
    -	0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    -	0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e,
    -	0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    -	0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65,
    -	0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
    -	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74,
    -	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73,
    -	0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
    -	0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73,
    -	0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    -	0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65,
    -	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72,
    -	0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54,
    -	0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06,
    -	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
    +	0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
    +	0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67,
    +	0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65,
    +	0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c,
    +	0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71,
    +	0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69,
    +	0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    +	0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65,
    +	0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53,
    +	0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
    +	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c,
    +	0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
    +	0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65,
    +	0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    +	0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12,
    +	0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63,
    +	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50,
    +	0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63,
    +	0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61,
    +	0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    +	0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61,
    +	0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44,
    +	0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74,
    +	0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63,
    +	0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74,
    +	0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74,
    +	0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76,
    +	0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65,
    +	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f,
    +	0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
     }
     
     var (
    @@ -3393,117 +3798,134 @@ func file_daemon_proto_rawDescGZIP() []byte {
     	return file_daemon_proto_rawDescData
     }
     
    -var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
    -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 45)
    +var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
    +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 50)
     var file_daemon_proto_goTypes = []interface{}{
     	(LogLevel)(0),                            // 0: daemon.LogLevel
    -	(*LoginRequest)(nil),                     // 1: daemon.LoginRequest
    -	(*LoginResponse)(nil),                    // 2: daemon.LoginResponse
    -	(*WaitSSOLoginRequest)(nil),              // 3: daemon.WaitSSOLoginRequest
    -	(*WaitSSOLoginResponse)(nil),             // 4: daemon.WaitSSOLoginResponse
    -	(*UpRequest)(nil),                        // 5: daemon.UpRequest
    -	(*UpResponse)(nil),                       // 6: daemon.UpResponse
    -	(*StatusRequest)(nil),                    // 7: daemon.StatusRequest
    -	(*StatusResponse)(nil),                   // 8: daemon.StatusResponse
    -	(*DownRequest)(nil),                      // 9: daemon.DownRequest
    -	(*DownResponse)(nil),                     // 10: daemon.DownResponse
    -	(*GetConfigRequest)(nil),                 // 11: daemon.GetConfigRequest
    -	(*GetConfigResponse)(nil),                // 12: daemon.GetConfigResponse
    -	(*PeerState)(nil),                        // 13: daemon.PeerState
    -	(*LocalPeerState)(nil),                   // 14: daemon.LocalPeerState
    -	(*SignalState)(nil),                      // 15: daemon.SignalState
    -	(*ManagementState)(nil),                  // 16: daemon.ManagementState
    -	(*RelayState)(nil),                       // 17: daemon.RelayState
    -	(*NSGroupState)(nil),                     // 18: daemon.NSGroupState
    -	(*FullStatus)(nil),                       // 19: daemon.FullStatus
    -	(*ListNetworksRequest)(nil),              // 20: daemon.ListNetworksRequest
    -	(*ListNetworksResponse)(nil),             // 21: daemon.ListNetworksResponse
    -	(*SelectNetworksRequest)(nil),            // 22: daemon.SelectNetworksRequest
    -	(*SelectNetworksResponse)(nil),           // 23: daemon.SelectNetworksResponse
    -	(*IPList)(nil),                           // 24: daemon.IPList
    -	(*Network)(nil),                          // 25: daemon.Network
    -	(*DebugBundleRequest)(nil),               // 26: daemon.DebugBundleRequest
    -	(*DebugBundleResponse)(nil),              // 27: daemon.DebugBundleResponse
    -	(*GetLogLevelRequest)(nil),               // 28: daemon.GetLogLevelRequest
    -	(*GetLogLevelResponse)(nil),              // 29: daemon.GetLogLevelResponse
    -	(*SetLogLevelRequest)(nil),               // 30: daemon.SetLogLevelRequest
    -	(*SetLogLevelResponse)(nil),              // 31: daemon.SetLogLevelResponse
    -	(*State)(nil),                            // 32: daemon.State
    -	(*ListStatesRequest)(nil),                // 33: daemon.ListStatesRequest
    -	(*ListStatesResponse)(nil),               // 34: daemon.ListStatesResponse
    -	(*CleanStateRequest)(nil),                // 35: daemon.CleanStateRequest
    -	(*CleanStateResponse)(nil),               // 36: daemon.CleanStateResponse
    -	(*DeleteStateRequest)(nil),               // 37: daemon.DeleteStateRequest
    -	(*DeleteStateResponse)(nil),              // 38: daemon.DeleteStateResponse
    -	(*SetNetworkMapPersistenceRequest)(nil),  // 39: daemon.SetNetworkMapPersistenceRequest
    -	(*SetNetworkMapPersistenceResponse)(nil), // 40: daemon.SetNetworkMapPersistenceResponse
    -	(*TCPFlags)(nil),                         // 41: daemon.TCPFlags
    -	(*TracePacketRequest)(nil),               // 42: daemon.TracePacketRequest
    -	(*TraceStage)(nil),                       // 43: daemon.TraceStage
    -	(*TracePacketResponse)(nil),              // 44: daemon.TracePacketResponse
    -	nil,                                      // 45: daemon.Network.ResolvedIPsEntry
    -	(*durationpb.Duration)(nil),              // 46: google.protobuf.Duration
    -	(*timestamppb.Timestamp)(nil),            // 47: google.protobuf.Timestamp
    +	(SystemEvent_Severity)(0),                // 1: daemon.SystemEvent.Severity
    +	(SystemEvent_Category)(0),                // 2: daemon.SystemEvent.Category
    +	(*LoginRequest)(nil),                     // 3: daemon.LoginRequest
    +	(*LoginResponse)(nil),                    // 4: daemon.LoginResponse
    +	(*WaitSSOLoginRequest)(nil),              // 5: daemon.WaitSSOLoginRequest
    +	(*WaitSSOLoginResponse)(nil),             // 6: daemon.WaitSSOLoginResponse
    +	(*UpRequest)(nil),                        // 7: daemon.UpRequest
    +	(*UpResponse)(nil),                       // 8: daemon.UpResponse
    +	(*StatusRequest)(nil),                    // 9: daemon.StatusRequest
    +	(*StatusResponse)(nil),                   // 10: daemon.StatusResponse
    +	(*DownRequest)(nil),                      // 11: daemon.DownRequest
    +	(*DownResponse)(nil),                     // 12: daemon.DownResponse
    +	(*GetConfigRequest)(nil),                 // 13: daemon.GetConfigRequest
    +	(*GetConfigResponse)(nil),                // 14: daemon.GetConfigResponse
    +	(*PeerState)(nil),                        // 15: daemon.PeerState
    +	(*LocalPeerState)(nil),                   // 16: daemon.LocalPeerState
    +	(*SignalState)(nil),                      // 17: daemon.SignalState
    +	(*ManagementState)(nil),                  // 18: daemon.ManagementState
    +	(*RelayState)(nil),                       // 19: daemon.RelayState
    +	(*NSGroupState)(nil),                     // 20: daemon.NSGroupState
    +	(*FullStatus)(nil),                       // 21: daemon.FullStatus
    +	(*ListNetworksRequest)(nil),              // 22: daemon.ListNetworksRequest
    +	(*ListNetworksResponse)(nil),             // 23: daemon.ListNetworksResponse
    +	(*SelectNetworksRequest)(nil),            // 24: daemon.SelectNetworksRequest
    +	(*SelectNetworksResponse)(nil),           // 25: daemon.SelectNetworksResponse
    +	(*IPList)(nil),                           // 26: daemon.IPList
    +	(*Network)(nil),                          // 27: daemon.Network
    +	(*DebugBundleRequest)(nil),               // 28: daemon.DebugBundleRequest
    +	(*DebugBundleResponse)(nil),              // 29: daemon.DebugBundleResponse
    +	(*GetLogLevelRequest)(nil),               // 30: daemon.GetLogLevelRequest
    +	(*GetLogLevelResponse)(nil),              // 31: daemon.GetLogLevelResponse
    +	(*SetLogLevelRequest)(nil),               // 32: daemon.SetLogLevelRequest
    +	(*SetLogLevelResponse)(nil),              // 33: daemon.SetLogLevelResponse
    +	(*State)(nil),                            // 34: daemon.State
    +	(*ListStatesRequest)(nil),                // 35: daemon.ListStatesRequest
    +	(*ListStatesResponse)(nil),               // 36: daemon.ListStatesResponse
    +	(*CleanStateRequest)(nil),                // 37: daemon.CleanStateRequest
    +	(*CleanStateResponse)(nil),               // 38: daemon.CleanStateResponse
    +	(*DeleteStateRequest)(nil),               // 39: daemon.DeleteStateRequest
    +	(*DeleteStateResponse)(nil),              // 40: daemon.DeleteStateResponse
    +	(*SetNetworkMapPersistenceRequest)(nil),  // 41: daemon.SetNetworkMapPersistenceRequest
    +	(*SetNetworkMapPersistenceResponse)(nil), // 42: daemon.SetNetworkMapPersistenceResponse
    +	(*TCPFlags)(nil),                         // 43: daemon.TCPFlags
    +	(*TracePacketRequest)(nil),               // 44: daemon.TracePacketRequest
    +	(*TraceStage)(nil),                       // 45: daemon.TraceStage
    +	(*TracePacketResponse)(nil),              // 46: daemon.TracePacketResponse
    +	(*SubscribeRequest)(nil),                 // 47: daemon.SubscribeRequest
    +	(*SystemEvent)(nil),                      // 48: daemon.SystemEvent
    +	(*GetEventsRequest)(nil),                 // 49: daemon.GetEventsRequest
    +	(*GetEventsResponse)(nil),                // 50: daemon.GetEventsResponse
    +	nil,                                      // 51: daemon.Network.ResolvedIPsEntry
    +	nil,                                      // 52: daemon.SystemEvent.MetadataEntry
    +	(*durationpb.Duration)(nil),              // 53: google.protobuf.Duration
    +	(*timestamppb.Timestamp)(nil),            // 54: google.protobuf.Timestamp
     }
     var file_daemon_proto_depIdxs = []int32{
    -	46, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
    -	19, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
    -	47, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
    -	47, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
    -	46, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration
    -	16, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
    -	15, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState
    -	14, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState
    -	13, // 8: daemon.FullStatus.peers:type_name -> daemon.PeerState
    -	17, // 9: daemon.FullStatus.relays:type_name -> daemon.RelayState
    -	18, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState
    -	25, // 11: daemon.ListNetworksResponse.routes:type_name -> daemon.Network
    -	45, // 12: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry
    -	0,  // 13: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel
    -	0,  // 14: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel
    -	32, // 15: daemon.ListStatesResponse.states:type_name -> daemon.State
    -	41, // 16: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags
    -	43, // 17: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage
    -	24, // 18: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
    -	1,  // 19: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
    -	3,  // 20: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
    -	5,  // 21: daemon.DaemonService.Up:input_type -> daemon.UpRequest
    -	7,  // 22: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
    -	9,  // 23: daemon.DaemonService.Down:input_type -> daemon.DownRequest
    -	11, // 24: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
    -	20, // 25: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest
    -	22, // 26: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest
    -	22, // 27: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest
    -	26, // 28: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest
    -	28, // 29: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest
    -	30, // 30: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest
    -	33, // 31: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest
    -	35, // 32: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest
    -	37, // 33: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest
    -	39, // 34: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest
    -	42, // 35: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest
    -	2,  // 36: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
    -	4,  // 37: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
    -	6,  // 38: daemon.DaemonService.Up:output_type -> daemon.UpResponse
    -	8,  // 39: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
    -	10, // 40: daemon.DaemonService.Down:output_type -> daemon.DownResponse
    -	12, // 41: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
    -	21, // 42: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
    -	23, // 43: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
    -	23, // 44: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
    -	27, // 45: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
    -	29, // 46: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
    -	31, // 47: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
    -	34, // 48: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
    -	36, // 49: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
    -	38, // 50: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
    -	40, // 51: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse
    -	44, // 52: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
    -	36, // [36:53] is the sub-list for method output_type
    -	19, // [19:36] is the sub-list for method input_type
    -	19, // [19:19] is the sub-list for extension type_name
    -	19, // [19:19] is the sub-list for extension extendee
    -	0,  // [0:19] is the sub-list for field type_name
    +	53, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
    +	21, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
    +	54, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
    +	54, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
    +	53, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration
    +	18, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
    +	17, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState
    +	16, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState
    +	15, // 8: daemon.FullStatus.peers:type_name -> daemon.PeerState
    +	19, // 9: daemon.FullStatus.relays:type_name -> daemon.RelayState
    +	20, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState
    +	48, // 11: daemon.FullStatus.events:type_name -> daemon.SystemEvent
    +	27, // 12: daemon.ListNetworksResponse.routes:type_name -> daemon.Network
    +	51, // 13: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry
    +	0,  // 14: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel
    +	0,  // 15: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel
    +	34, // 16: daemon.ListStatesResponse.states:type_name -> daemon.State
    +	43, // 17: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags
    +	45, // 18: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage
    +	1,  // 19: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity
    +	2,  // 20: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category
    +	54, // 21: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp
    +	52, // 22: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry
    +	48, // 23: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent
    +	26, // 24: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
    +	3,  // 25: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
    +	5,  // 26: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
    +	7,  // 27: daemon.DaemonService.Up:input_type -> daemon.UpRequest
    +	9,  // 28: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
    +	11, // 29: daemon.DaemonService.Down:input_type -> daemon.DownRequest
    +	13, // 30: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
    +	22, // 31: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest
    +	24, // 32: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest
    +	24, // 33: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest
    +	28, // 34: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest
    +	30, // 35: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest
    +	32, // 36: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest
    +	35, // 37: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest
    +	37, // 38: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest
    +	39, // 39: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest
    +	41, // 40: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest
    +	44, // 41: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest
    +	47, // 42: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest
    +	49, // 43: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest
    +	4,  // 44: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
    +	6,  // 45: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
    +	8,  // 46: daemon.DaemonService.Up:output_type -> daemon.UpResponse
    +	10, // 47: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
    +	12, // 48: daemon.DaemonService.Down:output_type -> daemon.DownResponse
    +	14, // 49: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
    +	23, // 50: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
    +	25, // 51: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
    +	25, // 52: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
    +	29, // 53: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
    +	31, // 54: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
    +	33, // 55: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
    +	36, // 56: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
    +	38, // 57: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
    +	40, // 58: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
    +	42, // 59: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse
    +	46, // 60: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
    +	48, // 61: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent
    +	50, // 62: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse
    +	44, // [44:63] is the sub-list for method output_type
    +	25, // [25:44] is the sub-list for method input_type
    +	25, // [25:25] is the sub-list for extension type_name
    +	25, // [25:25] is the sub-list for extension extendee
    +	0,  // [0:25] is the sub-list for field type_name
     }
     
     func init() { file_daemon_proto_init() }
    @@ -4040,6 +4462,54 @@ func file_daemon_proto_init() {
     				return nil
     			}
     		}
    +		file_daemon_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} {
    +			switch v := v.(*SubscribeRequest); i {
    +			case 0:
    +				return &v.state
    +			case 1:
    +				return &v.sizeCache
    +			case 2:
    +				return &v.unknownFields
    +			default:
    +				return nil
    +			}
    +		}
    +		file_daemon_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} {
    +			switch v := v.(*SystemEvent); i {
    +			case 0:
    +				return &v.state
    +			case 1:
    +				return &v.sizeCache
    +			case 2:
    +				return &v.unknownFields
    +			default:
    +				return nil
    +			}
    +		}
    +		file_daemon_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} {
    +			switch v := v.(*GetEventsRequest); i {
    +			case 0:
    +				return &v.state
    +			case 1:
    +				return &v.sizeCache
    +			case 2:
    +				return &v.unknownFields
    +			default:
    +				return nil
    +			}
    +		}
    +		file_daemon_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} {
    +			switch v := v.(*GetEventsResponse); i {
    +			case 0:
    +				return &v.state
    +			case 1:
    +				return &v.sizeCache
    +			case 2:
    +				return &v.unknownFields
    +			default:
    +				return nil
    +			}
    +		}
     	}
     	file_daemon_proto_msgTypes[0].OneofWrappers = []interface{}{}
     	file_daemon_proto_msgTypes[41].OneofWrappers = []interface{}{}
    @@ -4049,8 +4519,8 @@ func file_daemon_proto_init() {
     		File: protoimpl.DescBuilder{
     			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
     			RawDescriptor: file_daemon_proto_rawDesc,
    -			NumEnums:      1,
    -			NumMessages:   45,
    +			NumEnums:      3,
    +			NumMessages:   50,
     			NumExtensions: 0,
     			NumServices:   1,
     		},
    diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto
    index 412449076..92a289c41 100644
    --- a/client/proto/daemon.proto
    +++ b/client/proto/daemon.proto
    @@ -59,6 +59,10 @@ service DaemonService {
       rpc SetNetworkMapPersistence(SetNetworkMapPersistenceRequest) returns (SetNetworkMapPersistenceResponse) {}
     
       rpc TracePacket(TracePacketRequest) returns (TracePacketResponse) {}
    +
    +  rpc SubscribeEvents(SubscribeRequest) returns (stream SystemEvent) {}
    +
    +  rpc GetEvents(GetEventsRequest) returns (GetEventsResponse) {}
     }
     
     
    @@ -116,6 +120,8 @@ message LoginRequest {
       optional bool disable_firewall = 23;
     
       optional bool block_lan_access = 24;
    +
    +  optional bool disable_notifications = 25;
     }
     
     message LoginResponse {
    @@ -181,6 +187,8 @@ message GetConfigResponse {
       bool rosenpassEnabled = 11;
     
       bool rosenpassPermissive = 12;
    +
    +  bool disable_notifications = 13;
     }
     
     // PeerState contains the latest state of a peer
    @@ -251,6 +259,8 @@ message FullStatus {
       repeated PeerState peers = 4;
       repeated RelayState relays = 5;
       repeated NSGroupState dns_servers = 6;
    +
    +  repeated SystemEvent events = 7;
     }
     
     message ListNetworksRequest {
    @@ -391,3 +401,35 @@ message TracePacketResponse {
       repeated TraceStage stages = 1;
       bool final_disposition = 2;
     }
    +
    +message SubscribeRequest{}
    +
    +message SystemEvent {
    +  enum Severity {
    +    INFO = 0;
    +    WARNING = 1;
    +    ERROR = 2;
    +    CRITICAL = 3;
    +  }
    +
    +  enum Category {
    +    NETWORK = 0;
    +    DNS = 1;
    +    AUTHENTICATION = 2;
    +    CONNECTIVITY = 3;
    +  }
    +
    +  string id = 1;
    +  Severity severity = 2;
    +  Category category = 3;
    +  string message = 4;
    +  string userMessage = 5;
    +  google.protobuf.Timestamp timestamp = 6;
    +  map metadata = 7;
    +}
    +
    +message GetEventsRequest {}
    +
    +message GetEventsResponse {
    +  repeated SystemEvent events = 1;
    +}
    diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go
    index 9dcb543a8..0cb2a7c59 100644
    --- a/client/proto/daemon_grpc.pb.go
    +++ b/client/proto/daemon_grpc.pb.go
    @@ -52,6 +52,8 @@ type DaemonServiceClient interface {
     	// SetNetworkMapPersistence enables or disables network map persistence
     	SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error)
     	TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error)
    +	SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error)
    +	GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error)
     }
     
     type daemonServiceClient struct {
    @@ -215,6 +217,47 @@ func (c *daemonServiceClient) TracePacket(ctx context.Context, in *TracePacketRe
     	return out, nil
     }
     
    +func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) {
    +	stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], "/daemon.DaemonService/SubscribeEvents", opts...)
    +	if err != nil {
    +		return nil, err
    +	}
    +	x := &daemonServiceSubscribeEventsClient{stream}
    +	if err := x.ClientStream.SendMsg(in); err != nil {
    +		return nil, err
    +	}
    +	if err := x.ClientStream.CloseSend(); err != nil {
    +		return nil, err
    +	}
    +	return x, nil
    +}
    +
    +type DaemonService_SubscribeEventsClient interface {
    +	Recv() (*SystemEvent, error)
    +	grpc.ClientStream
    +}
    +
    +type daemonServiceSubscribeEventsClient struct {
    +	grpc.ClientStream
    +}
    +
    +func (x *daemonServiceSubscribeEventsClient) Recv() (*SystemEvent, error) {
    +	m := new(SystemEvent)
    +	if err := x.ClientStream.RecvMsg(m); err != nil {
    +		return nil, err
    +	}
    +	return m, nil
    +}
    +
    +func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) {
    +	out := new(GetEventsResponse)
    +	err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetEvents", in, out, opts...)
    +	if err != nil {
    +		return nil, err
    +	}
    +	return out, nil
    +}
    +
     // DaemonServiceServer is the server API for DaemonService service.
     // All implementations must embed UnimplementedDaemonServiceServer
     // for forward compatibility
    @@ -253,6 +296,8 @@ type DaemonServiceServer interface {
     	// SetNetworkMapPersistence enables or disables network map persistence
     	SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error)
     	TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error)
    +	SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error
    +	GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error)
     	mustEmbedUnimplementedDaemonServiceServer()
     }
     
    @@ -311,6 +356,12 @@ func (UnimplementedDaemonServiceServer) SetNetworkMapPersistence(context.Context
     func (UnimplementedDaemonServiceServer) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) {
     	return nil, status.Errorf(codes.Unimplemented, "method TracePacket not implemented")
     }
    +func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error {
    +	return status.Errorf(codes.Unimplemented, "method SubscribeEvents not implemented")
    +}
    +func (UnimplementedDaemonServiceServer) GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) {
    +	return nil, status.Errorf(codes.Unimplemented, "method GetEvents not implemented")
    +}
     func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {}
     
     // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service.
    @@ -630,6 +681,45 @@ func _DaemonService_TracePacket_Handler(srv interface{}, ctx context.Context, de
     	return interceptor(ctx, in, info, handler)
     }
     
    +func _DaemonService_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
    +	m := new(SubscribeRequest)
    +	if err := stream.RecvMsg(m); err != nil {
    +		return err
    +	}
    +	return srv.(DaemonServiceServer).SubscribeEvents(m, &daemonServiceSubscribeEventsServer{stream})
    +}
    +
    +type DaemonService_SubscribeEventsServer interface {
    +	Send(*SystemEvent) error
    +	grpc.ServerStream
    +}
    +
    +type daemonServiceSubscribeEventsServer struct {
    +	grpc.ServerStream
    +}
    +
    +func (x *daemonServiceSubscribeEventsServer) Send(m *SystemEvent) error {
    +	return x.ServerStream.SendMsg(m)
    +}
    +
    +func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
    +	in := new(GetEventsRequest)
    +	if err := dec(in); err != nil {
    +		return nil, err
    +	}
    +	if interceptor == nil {
    +		return srv.(DaemonServiceServer).GetEvents(ctx, in)
    +	}
    +	info := &grpc.UnaryServerInfo{
    +		Server:     srv,
    +		FullMethod: "/daemon.DaemonService/GetEvents",
    +	}
    +	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
    +		return srv.(DaemonServiceServer).GetEvents(ctx, req.(*GetEventsRequest))
    +	}
    +	return interceptor(ctx, in, info, handler)
    +}
    +
     // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service.
     // It's only intended for direct use with grpc.RegisterService,
     // and not to be introspected or modified (even as a copy)
    @@ -705,7 +795,17 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
     			MethodName: "TracePacket",
     			Handler:    _DaemonService_TracePacket_Handler,
     		},
    +		{
    +			MethodName: "GetEvents",
    +			Handler:    _DaemonService_GetEvents_Handler,
    +		},
    +	},
    +	Streams: []grpc.StreamDesc{
    +		{
    +			StreamName:    "SubscribeEvents",
    +			Handler:       _DaemonService_SubscribeEvents_Handler,
    +			ServerStreams: true,
    +		},
     	},
    -	Streams:  []grpc.StreamDesc{},
     	Metadata: "daemon.proto",
     }
    diff --git a/client/server/event.go b/client/server/event.go
    new file mode 100644
    index 000000000..9a4e0fbf5
    --- /dev/null
    +++ b/client/server/event.go
    @@ -0,0 +1,36 @@
    +package server
    +
    +import (
    +	"context"
    +
    +	log "github.com/sirupsen/logrus"
    +
    +	"github.com/netbirdio/netbird/client/proto"
    +)
    +
    +func (s *Server) SubscribeEvents(req *proto.SubscribeRequest, stream proto.DaemonService_SubscribeEventsServer) error {
    +	subscription := s.statusRecorder.SubscribeToEvents()
    +	defer func() {
    +		s.statusRecorder.UnsubscribeFromEvents(subscription)
    +		log.Debug("client unsubscribed from events")
    +	}()
    +
    +	log.Debug("client subscribed to events")
    +
    +	for {
    +		select {
    +		case event := <-subscription.Events():
    +			if err := stream.Send(event); err != nil {
    +				log.Warnf("error sending event to %v: %v", req, err)
    +				return err
    +			}
    +		case <-stream.Context().Done():
    +			return nil
    +		}
    +	}
    +}
    +
    +func (s *Server) GetEvents(context.Context, *proto.GetEventsRequest) (*proto.GetEventsResponse, error) {
    +	events := s.statusRecorder.GetEventHistory()
    +	return &proto.GetEventsResponse{Events: events}, nil
    +}
    diff --git a/client/server/server.go b/client/server/server.go
    index 42420d1c1..9250b3e8b 100644
    --- a/client/server/server.go
    +++ b/client/server/server.go
    @@ -404,6 +404,11 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
     		s.latestConfigInput.BlockLANAccess = msg.BlockLanAccess
     	}
     
    +	if msg.DisableNotifications != nil {
    +		inputConfig.DisableNotifications = msg.DisableNotifications
    +		s.latestConfigInput.DisableNotifications = msg.DisableNotifications
    +	}
    +
     	s.mutex.Unlock()
     
     	if msg.OptionalPreSharedKey != nil {
    @@ -687,6 +692,7 @@ func (s *Server) Status(
     
     		fullStatus := s.statusRecorder.GetFullStatus()
     		pbFullStatus := toProtoFullStatus(fullStatus)
    +		pbFullStatus.Events = s.statusRecorder.GetEventHistory()
     		statusResponse.FullStatus = pbFullStatus
     	}
     
    @@ -736,17 +742,18 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto
     	}
     
     	return &proto.GetConfigResponse{
    -		ManagementUrl:       managementURL,
    -		ConfigFile:          s.latestConfigInput.ConfigPath,
    -		LogFile:             s.logFile,
    -		PreSharedKey:        preSharedKey,
    -		AdminURL:            adminURL,
    -		InterfaceName:       s.config.WgIface,
    -		WireguardPort:       int64(s.config.WgPort),
    -		DisableAutoConnect:  s.config.DisableAutoConnect,
    -		ServerSSHAllowed:    *s.config.ServerSSHAllowed,
    -		RosenpassEnabled:    s.config.RosenpassEnabled,
    -		RosenpassPermissive: s.config.RosenpassPermissive,
    +		ManagementUrl:        managementURL,
    +		ConfigFile:           s.latestConfigInput.ConfigPath,
    +		LogFile:              s.logFile,
    +		PreSharedKey:         preSharedKey,
    +		AdminURL:             adminURL,
    +		InterfaceName:        s.config.WgIface,
    +		WireguardPort:        int64(s.config.WgPort),
    +		DisableAutoConnect:   s.config.DisableAutoConnect,
    +		ServerSSHAllowed:     *s.config.ServerSSHAllowed,
    +		RosenpassEnabled:     s.config.RosenpassEnabled,
    +		RosenpassPermissive:  s.config.RosenpassPermissive,
    +		DisableNotifications: s.config.DisableNotifications,
     	}, nil
     }
     func (s *Server) onSessionExpire() {
    diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go
    index 618160128..9ed40b0be 100644
    --- a/client/ui/client_ui.go
    +++ b/client/ui/client_ui.go
    @@ -34,6 +34,7 @@ import (
     	"github.com/netbirdio/netbird/client/internal"
     	"github.com/netbirdio/netbird/client/proto"
     	"github.com/netbirdio/netbird/client/system"
    +	"github.com/netbirdio/netbird/client/ui/event"
     	"github.com/netbirdio/netbird/util"
     	"github.com/netbirdio/netbird/version"
     )
    @@ -161,6 +162,7 @@ type serviceClient struct {
     	mAllowSSH         *systray.MenuItem
     	mAutoConnect      *systray.MenuItem
     	mEnableRosenpass  *systray.MenuItem
    +	mNotifications    *systray.MenuItem
     	mAdvancedSettings *systray.MenuItem
     
     	// application with main windows.
    @@ -196,6 +198,8 @@ type serviceClient struct {
     	isUpdateIconActive   bool
     	showRoutes           bool
     	wRoutes              fyne.Window
    +
    +	eventManager *event.Manager
     }
     
     // newServiceClient instance constructor
    @@ -429,6 +433,7 @@ func (s *serviceClient) menuUpClick() error {
     		log.Errorf("up service: %v", err)
     		return err
     	}
    +
     	return nil
     }
     
    @@ -570,6 +575,7 @@ func (s *serviceClient) onTrayReady() {
     	s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", "Allow SSH connections", false)
     	s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", "Connect automatically when the service starts", false)
     	s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", "Enable post-quantum security via Rosenpass", false)
    +	s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", "Enable notifications", true)
     	s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", "Advanced settings of the application")
     	s.loadSettings()
     
    @@ -606,6 +612,10 @@ func (s *serviceClient) onTrayReady() {
     		}
     	}()
     
    +	s.eventManager = event.NewManager(s.app, s.addr)
    +	s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
    +	go s.eventManager.Start(s.ctx)
    +
     	go func() {
     		var err error
     		for {
    @@ -680,7 +690,20 @@ func (s *serviceClient) onTrayReady() {
     					defer s.mRoutes.Enable()
     					s.runSelfCommand("networks", "true")
     				}()
    +			case <-s.mNotifications.ClickedCh:
    +				if s.mNotifications.Checked() {
    +					s.mNotifications.Uncheck()
    +				} else {
    +					s.mNotifications.Check()
    +				}
    +				if s.eventManager != nil {
    +					s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
    +				}
    +				if err := s.updateConfig(); err != nil {
    +					log.Errorf("failed to update config: %v", err)
    +				}
     			}
    +
     			if err != nil {
     				log.Errorf("process connection: %v", err)
     			}
    @@ -780,8 +803,20 @@ func (s *serviceClient) getSrvConfig() {
     		if !cfg.RosenpassEnabled {
     			s.sRosenpassPermissive.Disable()
     		}
    -
     	}
    +
    +	if s.mNotifications == nil {
    +		return
    +	}
    +	if cfg.DisableNotifications {
    +		s.mNotifications.Uncheck()
    +	} else {
    +		s.mNotifications.Check()
    +	}
    +	if s.eventManager != nil {
    +		s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
    +	}
    +
     }
     
     func (s *serviceClient) onUpdateAvailable() {
    @@ -846,6 +881,15 @@ func (s *serviceClient) loadSettings() {
     	} else {
     		s.mEnableRosenpass.Uncheck()
     	}
    +
    +	if cfg.DisableNotifications {
    +		s.mNotifications.Uncheck()
    +	} else {
    +		s.mNotifications.Check()
    +	}
    +	if s.eventManager != nil {
    +		s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
    +	}
     }
     
     // updateConfig updates the configuration parameters
    @@ -854,12 +898,14 @@ func (s *serviceClient) updateConfig() error {
     	disableAutoStart := !s.mAutoConnect.Checked()
     	sshAllowed := s.mAllowSSH.Checked()
     	rosenpassEnabled := s.mEnableRosenpass.Checked()
    +	notificationsDisabled := !s.mNotifications.Checked()
     
     	loginRequest := proto.LoginRequest{
     		IsLinuxDesktopClient: runtime.GOOS == "linux",
     		ServerSSHAllowed:     &sshAllowed,
     		RosenpassEnabled:     &rosenpassEnabled,
     		DisableAutoConnect:   &disableAutoStart,
    +		DisableNotifications: ¬ificationsDisabled,
     	}
     
     	if err := s.restartClient(&loginRequest); err != nil {
    diff --git a/client/ui/event/event.go b/client/ui/event/event.go
    new file mode 100644
    index 000000000..7925ee4d3
    --- /dev/null
    +++ b/client/ui/event/event.go
    @@ -0,0 +1,151 @@
    +package event
    +
    +import (
    +	"context"
    +	"fmt"
    +	"strings"
    +	"sync"
    +	"time"
    +
    +	"fyne.io/fyne/v2"
    +	"github.com/cenkalti/backoff/v4"
    +	log "github.com/sirupsen/logrus"
    +	"google.golang.org/grpc"
    +	"google.golang.org/grpc/credentials/insecure"
    +
    +	"github.com/netbirdio/netbird/client/proto"
    +	"github.com/netbirdio/netbird/client/system"
    +)
    +
    +type Manager struct {
    +	app  fyne.App
    +	addr string
    +
    +	mu      sync.Mutex
    +	ctx     context.Context
    +	cancel  context.CancelFunc
    +	enabled bool
    +}
    +
    +func NewManager(app fyne.App, addr string) *Manager {
    +	return &Manager{
    +		app:  app,
    +		addr: addr,
    +	}
    +}
    +
    +func (e *Manager) Start(ctx context.Context) {
    +	e.mu.Lock()
    +	e.ctx, e.cancel = context.WithCancel(ctx)
    +	e.mu.Unlock()
    +
    +	expBackOff := backoff.WithContext(&backoff.ExponentialBackOff{
    +		InitialInterval:     time.Second,
    +		RandomizationFactor: backoff.DefaultRandomizationFactor,
    +		Multiplier:          backoff.DefaultMultiplier,
    +		MaxInterval:         10 * time.Second,
    +		MaxElapsedTime:      0,
    +		Stop:                backoff.Stop,
    +		Clock:               backoff.SystemClock,
    +	}, ctx)
    +
    +	if err := backoff.Retry(e.streamEvents, expBackOff); err != nil {
    +		log.Errorf("event stream ended: %v", err)
    +	}
    +}
    +
    +func (e *Manager) streamEvents() error {
    +	e.mu.Lock()
    +	ctx := e.ctx
    +	e.mu.Unlock()
    +
    +	client, err := getClient(e.addr)
    +	if err != nil {
    +		return fmt.Errorf("create client: %w", err)
    +	}
    +
    +	stream, err := client.SubscribeEvents(ctx, &proto.SubscribeRequest{})
    +	if err != nil {
    +		return fmt.Errorf("failed to subscribe to events: %w", err)
    +	}
    +
    +	log.Info("subscribed to daemon events")
    +	defer func() {
    +		log.Info("unsubscribed from daemon events")
    +	}()
    +
    +	for {
    +		event, err := stream.Recv()
    +		if err != nil {
    +			return fmt.Errorf("error receiving event: %w", err)
    +		}
    +		e.handleEvent(event)
    +	}
    +}
    +
    +func (e *Manager) Stop() {
    +	e.mu.Lock()
    +	defer e.mu.Unlock()
    +	if e.cancel != nil {
    +		e.cancel()
    +	}
    +}
    +
    +func (e *Manager) SetNotificationsEnabled(enabled bool) {
    +	e.mu.Lock()
    +	defer e.mu.Unlock()
    +	e.enabled = enabled
    +}
    +
    +func (e *Manager) handleEvent(event *proto.SystemEvent) {
    +	e.mu.Lock()
    +	enabled := e.enabled
    +	e.mu.Unlock()
    +
    +	if !enabled {
    +		return
    +	}
    +
    +	title := e.getEventTitle(event)
    +	e.app.SendNotification(fyne.NewNotification(title, event.UserMessage))
    +}
    +
    +func (e *Manager) getEventTitle(event *proto.SystemEvent) string {
    +	var prefix string
    +	switch event.Severity {
    +	case proto.SystemEvent_ERROR, proto.SystemEvent_CRITICAL:
    +		prefix = "Error"
    +	case proto.SystemEvent_WARNING:
    +		prefix = "Warning"
    +	default:
    +		prefix = "Info"
    +	}
    +
    +	var category string
    +	switch event.Category {
    +	case proto.SystemEvent_DNS:
    +		category = "DNS"
    +	case proto.SystemEvent_NETWORK:
    +		category = "Network"
    +	case proto.SystemEvent_AUTHENTICATION:
    +		category = "Authentication"
    +	case proto.SystemEvent_CONNECTIVITY:
    +		category = "Connectivity"
    +	default:
    +		category = "System"
    +	}
    +
    +	return fmt.Sprintf("%s: %s", prefix, category)
    +}
    +
    +func getClient(addr string) (proto.DaemonServiceClient, error) {
    +	conn, err := grpc.NewClient(
    +		strings.TrimPrefix(addr, "tcp://"),
    +		grpc.WithTransportCredentials(insecure.NewCredentials()),
    +		grpc.WithUserAgent(system.GetDesktopUIUserAgent()),
    +	)
    +	if err != nil {
    +		return nil, err
    +	}
    +	return proto.NewDaemonServiceClient(conn), nil
    +}
    
    From 39986b0e9757997032e4dc9460ce0e63b04032c8 Mon Sep 17 00:00:00 2001
    From: hakansa <43675540+hakansa@users.noreply.github.com>
    Date: Thu, 20 Feb 2025 13:43:20 +0300
    Subject: [PATCH 14/23] [client, management] Support DNS Labels for Peer
     Addressing (#3252)
    
    * [client] Support Extra DNS Labels for Peer Addressing
    
    * [management] Support Extra DNS Labels for Peer Addressing
    
    ---------
    
    Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com>
    ---
     client/cmd/login.go                           |   6 +
     client/cmd/up.go                              |  46 +-
     client/internal/config.go                     |  14 +
     client/internal/connect.go                    |   2 +-
     client/internal/dns/local.go                  |  70 +-
     client/internal/dns/local_test.go             |   2 +-
     client/internal/dns/server.go                 |  39 +-
     client/internal/dns/server_test.go            |   2 +-
     client/internal/login.go                      |   2 +-
     client/proto/daemon.pb.go                     | 928 +++++++++---------
     client/proto/daemon.proto                     |   8 +
     client/server/server.go                       |  10 +
     management/client/client.go                   |   3 +-
     management/client/client_test.go              |   2 +-
     management/client/grpc.go                     |   5 +-
     management/client/mock.go                     |   7 +-
     management/domain/validate.go                 |  65 ++
     management/domain/validate_test.go            | 206 ++++
     management/proto/management.pb.go             | 792 +++++++--------
     management/proto/management.proto             |   3 +-
     management/server/account.go                  |   2 +-
     management/server/account_test.go             |   6 +-
     management/server/grpcserver.go               |   1 +
     management/server/http/api/openapi.yml        |  16 +
     management/server/http/api/types.gen.go       |  18 +
     .../http/handlers/peers/peers_handler.go      |  10 +
     .../http/handlers/routes/routes_handler.go    |  39 +-
     .../handlers/routes/routes_handler_test.go    |  90 --
     .../handlers/setup_keys/setupkeys_handler.go  |  37 +-
     .../setup_keys/setupkeys_handler_test.go      |   5 +-
     management/server/management_proto_test.go    |   2 +-
     management/server/mock_server/account_mock.go |   5 +-
     management/server/peer.go                     |  26 +
     management/server/peer/peer.go                |   7 +
     management/server/peer_test.go                |   6 +-
     management/server/setupkey.go                 |   4 +-
     management/server/setupkey_test.go            |  24 +-
     management/server/types/account.go            |  17 +-
     management/server/types/setupkey.go           |  65 +-
     39 files changed, 1504 insertions(+), 1088 deletions(-)
     create mode 100644 management/domain/validate.go
     create mode 100644 management/domain/validate_test.go
    
    diff --git a/client/cmd/login.go b/client/cmd/login.go
    index c7dd0fda1..b91cedede 100644
    --- a/client/cmd/login.go
    +++ b/client/cmd/login.go
    @@ -85,11 +85,17 @@ var loginCmd = &cobra.Command{
     
     		client := proto.NewDaemonServiceClient(conn)
     
    +		var dnsLabelsReq []string
    +		if dnsLabelsValidated != nil {
    +			dnsLabelsReq = dnsLabelsValidated.ToSafeStringList()
    +		}
    +
     		loginRequest := proto.LoginRequest{
     			SetupKey:             providedSetupKey,
     			ManagementUrl:        managementURL,
     			IsLinuxDesktopClient: isLinuxRunningDesktop(),
     			Hostname:             hostName,
    +			DnsLabels:            dnsLabelsReq,
     		}
     
     		if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
    diff --git a/client/cmd/up.go b/client/cmd/up.go
    index f7c2bbfe4..926317b8e 100644
    --- a/client/cmd/up.go
    +++ b/client/cmd/up.go
    @@ -20,6 +20,7 @@ import (
     	"github.com/netbirdio/netbird/client/internal/peer"
     	"github.com/netbirdio/netbird/client/proto"
     	"github.com/netbirdio/netbird/client/system"
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/util"
     )
     
    @@ -29,9 +30,16 @@ const (
     	interfaceInputType
     )
     
    +const (
    +	dnsLabelsFlag = "extra-dns-labels"
    +)
    +
     var (
    -	foregroundMode bool
    -	upCmd          = &cobra.Command{
    +	foregroundMode     bool
    +	dnsLabels          []string
    +	dnsLabelsValidated domain.List
    +
    +	upCmd = &cobra.Command{
     		Use:   "up",
     		Short: "install, login and start Netbird client",
     		RunE:  upFunc,
    @@ -49,6 +57,14 @@ func init() {
     	upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
     	upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval")
     	upCmd.PersistentFlags().BoolVar(&blockLANAccess, blockLANAccessFlag, false, "Block access to local networks (LAN) when using this peer as a router or exit node")
    +
    +	upCmd.PersistentFlags().StringSliceVar(&dnsLabels, dnsLabelsFlag, nil,
    +		`Sets DNS labels`+
    +			`You can specify a comma-separated list of up to 32 labels. `+
    +			`An empty string "" clears the previous configuration. `+
    +			`E.g. --extra-dns-labels vpc1 or --extra-dns-labels vpc1,mgmt1 `+
    +			`or --extra-dns-labels ""`,
    +	)
     }
     
     func upFunc(cmd *cobra.Command, args []string) error {
    @@ -67,6 +83,11 @@ func upFunc(cmd *cobra.Command, args []string) error {
     		return err
     	}
     
    +	dnsLabelsValidated, err = validateDnsLabels(dnsLabels)
    +	if err != nil {
    +		return err
    +	}
    +
     	ctx := internal.CtxInitState(cmd.Context())
     
     	if hostName != "" {
    @@ -98,6 +119,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
     		NATExternalIPs:      natExternalIPs,
     		CustomDNSAddress:    customDNSAddressConverted,
     		ExtraIFaceBlackList: extraIFaceBlackList,
    +		DNSLabels:           dnsLabelsValidated,
     	}
     
     	if cmd.Flag(enableRosenpassFlag).Changed {
    @@ -240,6 +262,8 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
     		IsLinuxDesktopClient: isLinuxRunningDesktop(),
     		Hostname:             hostName,
     		ExtraIFaceBlacklist:  extraIFaceBlackList,
    +		DnsLabels:            dnsLabels,
    +		CleanDNSLabels:       dnsLabels != nil && len(dnsLabels) == 0,
     	}
     
     	if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
    @@ -430,6 +454,24 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) {
     	return parsed, nil
     }
     
    +func validateDnsLabels(labels []string) (domain.List, error) {
    +	var (
    +		domains domain.List
    +		err     error
    +	)
    +
    +	if len(labels) == 0 {
    +		return domains, nil
    +	}
    +
    +	domains, err = domain.ValidateDomains(labels)
    +	if err != nil {
    +		return nil, fmt.Errorf("failed to validate dns labels: %v", err)
    +	}
    +
    +	return domains, nil
    +}
    +
     func isValidAddrPort(input string) bool {
     	if input == "" {
     		return true
    diff --git a/client/internal/config.go b/client/internal/config.go
    index 5703539cc..b269a3854 100644
    --- a/client/internal/config.go
    +++ b/client/internal/config.go
    @@ -8,6 +8,7 @@ import (
     	"os"
     	"reflect"
     	"runtime"
    +	"slices"
     	"strings"
     	"time"
     
    @@ -20,6 +21,7 @@ import (
     	"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
     	"github.com/netbirdio/netbird/client/ssh"
     	mgm "github.com/netbirdio/netbird/management/client"
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/util"
     )
     
    @@ -70,6 +72,8 @@ type ConfigInput struct {
     	BlockLANAccess *bool
     
     	DisableNotifications *bool
    +
    +	DNSLabels domain.List
     }
     
     // Config Configuration type
    @@ -97,6 +101,8 @@ type Config struct {
     
     	DisableNotifications bool
     
    +	DNSLabels domain.List
    +
     	// SSHKey is a private SSH key in a PEM format
     	SSHKey string
     
    @@ -503,6 +509,14 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
     		}
     	}
     
    +	if input.DNSLabels != nil && !slices.Equal(config.DNSLabels, input.DNSLabels) {
    +		log.Infof("updating DNS labels [ %s ] (old value: [ %s ])",
    +			input.DNSLabels.SafeString(),
    +			config.DNSLabels.SafeString())
    +		config.DNSLabels = input.DNSLabels
    +		updated = true
    +	}
    +
     	return updated, nil
     }
     
    diff --git a/client/internal/connect.go b/client/internal/connect.go
    index a0d585ffe..26ae3b687 100644
    --- a/client/internal/connect.go
    +++ b/client/internal/connect.go
    @@ -478,7 +478,7 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte,
     		config.DisableDNS,
     		config.DisableFirewall,
     	)
    -	loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey)
    +	loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey, config.DNSLabels)
     	if err != nil {
     		return nil, err
     	}
    diff --git a/client/internal/dns/local.go b/client/internal/dns/local.go
    index 80113885a..3a25a23b6 100644
    --- a/client/internal/dns/local.go
    +++ b/client/internal/dns/local.go
    @@ -15,7 +15,7 @@ type registrationMap map[string]struct{}
     
     type localResolver struct {
     	registeredMap registrationMap
    -	records       sync.Map
    +	records       sync.Map // key: string (domain_class_type), value: []dns.RR
     }
     
     func (d *localResolver) MatchSubdomains() bool {
    @@ -44,11 +44,12 @@ func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
     	replyMessage := &dns.Msg{}
     	replyMessage.SetReply(r)
     	replyMessage.RecursionAvailable = true
    -	replyMessage.Rcode = dns.RcodeSuccess
     
    -	response := d.lookupRecord(r)
    -	if response != nil {
    -		replyMessage.Answer = append(replyMessage.Answer, response)
    +	// lookup all records matching the question
    +	records := d.lookupRecords(r)
    +	if len(records) > 0 {
    +		replyMessage.Rcode = dns.RcodeSuccess
    +		replyMessage.Answer = append(replyMessage.Answer, records...)
     	} else {
     		replyMessage.Rcode = dns.RcodeNameError
     	}
    @@ -59,38 +60,65 @@ func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
     	}
     }
     
    -func (d *localResolver) lookupRecord(r *dns.Msg) dns.RR {
    +// lookupRecords fetches *all* DNS records matching the first question in r.
    +func (d *localResolver) lookupRecords(r *dns.Msg) []dns.RR {
    +	if len(r.Question) == 0 {
    +		return nil
    +	}
     	question := r.Question[0]
     	question.Name = strings.ToLower(question.Name)
    -	record, found := d.records.Load(buildRecordKey(question.Name, question.Qclass, question.Qtype))
    +	key := buildRecordKey(question.Name, question.Qclass, question.Qtype)
    +
    +	value, found := d.records.Load(key)
     	if !found {
     		return nil
     	}
     
    -	return record.(dns.RR)
    -}
    -
    -func (d *localResolver) registerRecord(record nbdns.SimpleRecord) error {
    -	fullRecord, err := dns.NewRR(record.String())
    -	if err != nil {
    -		return fmt.Errorf("register record: %w", err)
    +	records, ok := value.([]dns.RR)
    +	if !ok {
    +		log.Errorf("failed to cast records to []dns.RR, records: %v", value)
    +		return nil
     	}
     
    -	fullRecord.Header().Rdlength = record.Len()
    +	// if there's more than one record, rotate them (round-robin)
    +	if len(records) > 1 {
    +		first := records[0]
    +		records = append(records[1:], first)
    +		d.records.Store(key, records)
    +	}
     
    -	header := fullRecord.Header()
    -	d.records.Store(buildRecordKey(header.Name, header.Class, header.Rrtype), fullRecord)
    -
    -	return nil
    +	return records
     }
     
    +// registerRecord stores a new record by appending it to any existing list
    +func (d *localResolver) registerRecord(record nbdns.SimpleRecord) (string, error) {
    +	rr, err := dns.NewRR(record.String())
    +	if err != nil {
    +		return "", fmt.Errorf("register record: %w", err)
    +	}
    +
    +	rr.Header().Rdlength = record.Len()
    +	header := rr.Header()
    +	key := buildRecordKey(header.Name, header.Class, header.Rrtype)
    +
    +	// load any existing slice of records, then append
    +	existing, _ := d.records.LoadOrStore(key, []dns.RR{})
    +	records := existing.([]dns.RR)
    +	records = append(records, rr)
    +
    +	// store updated slice
    +	d.records.Store(key, records)
    +	return key, nil
    +}
    +
    +// deleteRecord removes *all* records under the recordKey.
     func (d *localResolver) deleteRecord(recordKey string) {
     	d.records.Delete(dns.Fqdn(recordKey))
     }
     
    +// buildRecordKey consistently generates a key: name_class_type
     func buildRecordKey(name string, class, qType uint16) string {
    -	key := fmt.Sprintf("%s_%d_%d", name, class, qType)
    -	return key
    +	return fmt.Sprintf("%s_%d_%d", dns.Fqdn(name), class, qType)
     }
     
     func (d *localResolver) probeAvailability() {}
    diff --git a/client/internal/dns/local_test.go b/client/internal/dns/local_test.go
    index b62cd66a9..0a42b321a 100644
    --- a/client/internal/dns/local_test.go
    +++ b/client/internal/dns/local_test.go
    @@ -55,7 +55,7 @@ func TestLocalResolver_ServeDNS(t *testing.T) {
     			resolver := &localResolver{
     				registeredMap: make(registrationMap),
     			}
    -			_ = resolver.registerRecord(testCase.inputRecord)
    +			_, _ = resolver.registerRecord(testCase.inputRecord)
     			var responseMSG *dns.Msg
     			responseWriter := &mockResponseWriter{
     				WriteMsgFunc: func(m *dns.Msg) error {
    diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go
    index fb94e07ac..d4d68370d 100644
    --- a/client/internal/dns/server.go
    +++ b/client/internal/dns/server.go
    @@ -393,10 +393,11 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
     		s.service.Stop()
     	}
     
    -	localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
    +	localMuxUpdates, localRecordsByDomain, err := s.buildLocalHandlerUpdate(update.CustomZones)
     	if err != nil {
     		return fmt.Errorf("not applying dns update, error: %v", err)
     	}
    +
     	upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups)
     	if err != nil {
     		return fmt.Errorf("not applying dns update, error: %v", err)
    @@ -404,7 +405,10 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
     	muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...) //nolint:gocritic
     
     	s.updateMux(muxUpdates)
    -	s.updateLocalResolver(localRecords)
    +
    +	// register local records
    +	s.updateLocalResolver(localRecordsByDomain)
    +
     	s.currentConfig = dnsConfigToHostDNSConfig(update, s.service.RuntimeIP(), s.service.RuntimePort())
     
     	hostUpdate := s.currentConfig
    @@ -434,9 +438,12 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
     	return nil
     }
     
    -func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]handlerWrapper, map[string]nbdns.SimpleRecord, error) {
    +func (s *DefaultServer) buildLocalHandlerUpdate(
    +	customZones []nbdns.CustomZone,
    +) ([]handlerWrapper, map[string][]nbdns.SimpleRecord, error) {
    +
     	var muxUpdates []handlerWrapper
    -	localRecords := make(map[string]nbdns.SimpleRecord, 0)
    +	localRecords := make(map[string][]nbdns.SimpleRecord)
     
     	for _, customZone := range customZones {
     		if len(customZone.Records) == 0 {
    @@ -449,6 +456,7 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone)
     			priority: PriorityMatchDomain,
     		})
     
    +		// group all records under this domain
     		for _, record := range customZone.Records {
     			var class uint16 = dns.ClassINET
     			if record.Class != nbdns.DefaultClass {
    @@ -456,9 +464,11 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone)
     			}
     
     			key := buildRecordKey(record.Name, class, uint16(record.Type))
    -			localRecords[key] = record
    +
    +			localRecords[key] = append(localRecords[key], record)
     		}
     	}
    +
     	return muxUpdates, localRecords, nil
     }
     
    @@ -594,7 +604,8 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
     	s.dnsMuxMap = muxUpdateMap
     }
     
    -func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
    +func (s *DefaultServer) updateLocalResolver(update map[string][]nbdns.SimpleRecord) {
    +	// remove old records that are no longer present
     	for key := range s.localResolver.registeredMap {
     		_, found := update[key]
     		if !found {
    @@ -603,12 +614,18 @@ func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord
     	}
     
     	updatedMap := make(registrationMap)
    -	for key, record := range update {
    -		err := s.localResolver.registerRecord(record)
    -		if err != nil {
    -			log.Warnf("got an error while registering the record (%s), error: %v", record.String(), err)
    +	for _, recs := range update {
    +		for _, rec := range recs {
    +			// convert the record to a dns.RR and register
    +			key, err := s.localResolver.registerRecord(rec)
    +			if err != nil {
    +				log.Warnf("got an error while registering the record (%s), error: %v",
    +					rec.String(), err)
    +				continue
    +			}
    +
    +			updatedMap[key] = struct{}{}
     		}
    -		updatedMap[key] = struct{}{}
     	}
     
     	s.localResolver.registeredMap = updatedMap
    diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go
    index db49f96a2..e9ddd5f59 100644
    --- a/client/internal/dns/server_test.go
    +++ b/client/internal/dns/server_test.go
    @@ -573,7 +573,7 @@ func TestDNSServerStartStop(t *testing.T) {
     			}
     			time.Sleep(100 * time.Millisecond)
     			defer dnsServer.Stop()
    -			err = dnsServer.localResolver.registerRecord(zoneRecords[0])
    +			_, err = dnsServer.localResolver.registerRecord(zoneRecords[0])
     			if err != nil {
     				t.Error(err)
     			}
    diff --git a/client/internal/login.go b/client/internal/login.go
    index b4ab1e363..092f2309c 100644
    --- a/client/internal/login.go
    +++ b/client/internal/login.go
    @@ -117,7 +117,7 @@ func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte
     		config.DisableDNS,
     		config.DisableFirewall,
     	)
    -	_, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey)
    +	_, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey, config.DNSLabels)
     	return serverKey, err
     }
     
    diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go
    index b40f6beea..3aa57da8f 100644
    --- a/client/proto/daemon.pb.go
    +++ b/client/proto/daemon.pb.go
    @@ -232,6 +232,11 @@ type LoginRequest struct {
     	DisableFirewall      *bool                `protobuf:"varint,23,opt,name=disable_firewall,json=disableFirewall,proto3,oneof" json:"disable_firewall,omitempty"`
     	BlockLanAccess       *bool                `protobuf:"varint,24,opt,name=block_lan_access,json=blockLanAccess,proto3,oneof" json:"block_lan_access,omitempty"`
     	DisableNotifications *bool                `protobuf:"varint,25,opt,name=disable_notifications,json=disableNotifications,proto3,oneof" json:"disable_notifications,omitempty"`
    +	DnsLabels            []string             `protobuf:"bytes,26,rep,name=dns_labels,json=dnsLabels,proto3" json:"dns_labels,omitempty"`
    +	// cleanDNSLabels clean map list of DNS labels.
    +	// This is needed because the generated code
    +	// omits initialized empty slices due to omitempty tags
    +	CleanDNSLabels bool `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"`
     }
     
     func (x *LoginRequest) Reset() {
    @@ -442,6 +447,20 @@ func (x *LoginRequest) GetDisableNotifications() bool {
     	return false
     }
     
    +func (x *LoginRequest) GetDnsLabels() []string {
    +	if x != nil {
    +		return x.DnsLabels
    +	}
    +	return nil
    +}
    +
    +func (x *LoginRequest) GetCleanDNSLabels() bool {
    +	if x != nil {
    +		return x.CleanDNSLabels
    +	}
    +	return false
    +}
    +
     type LoginResponse struct {
     	state         protoimpl.MessageState
     	sizeCache     protoimpl.SizeCache
    @@ -3251,7 +3270,7 @@ var file_daemon_proto_rawDesc = []byte{
     	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
     	0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
     	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74,
    -	0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe9, 0x0b, 0x0a, 0x0c, 0x4c, 0x6f,
    +	0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb0, 0x0c, 0x0a, 0x0c, 0x4c, 0x6f,
     	0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65,
     	0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65,
     	0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61,
    @@ -3325,465 +3344,470 @@ var file_daemon_proto_rawDesc = []byte{
     	0x73, 0x73, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
     	0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x19,
     	0x20, 0x01, 0x28, 0x08, 0x48, 0x0e, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e,
    -	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x42,
    -	0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
    -	0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61,
    -	0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67,
    -	0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74,
    -	0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65,
    -	0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74,
    -	0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72,
    -	0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a,
    -	0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69,
    -	0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73,
    -	0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a,
    -	0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74,
    -	0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61,
    -	0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65,
    -	0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e,
    -	0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69,
    -	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
    -	0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f,
    -	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
    -	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
    -	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73,
    -	0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d,
    -	0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a,
    -	0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72,
    -	0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    -	0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
    -	0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
    -	0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a,
    -	0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    -	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    -	0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14,
    -	0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70,
    -	0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    -	0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
    -	0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
    -	0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53,
    -	0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74,
    -	0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82,
    -	0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    -	0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c,
    -	0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
    -	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75,
    -	0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a,
    -	0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73,
    -	0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
    -	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xee, 0x03, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f,
    -	0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d,
    -	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55,
    -	0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65,
    -	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69,
    -	0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20,
    -	0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c,
    -	0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79,
    -	0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d,
    -	0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20,
    -	0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61,
    -	0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50,
    -	0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67,
    -	0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61,
    -	0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74,
    -	0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76,
    -	0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c,
    -	0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
    -	0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10,
    -	0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
    -	0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72,
    -	0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72,
    -	0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69,
    -	0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f,
    -	0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28,
    -	0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69,
    -	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72,
    -	0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
    -	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a,
    -	0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a,
    -	0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74,
    -	0x65, 0x18, 0x04, 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, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55,
    -	0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64,
    -	0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12,
    -	0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69,
    -	0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15,
    -	0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74,
    -	0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49,
    +	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12,
    +	0x1d, 0x0a, 0x0a, 0x64, 0x6e, 0x73, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1a, 0x20,
    +	0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x26,
    +	0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73,
    +	0x18, 0x1b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53,
    +	0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    +	0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f,
    +	0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a,
    +	0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42,
    +	0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53,
    +	0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73,
    +	0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42,
    +	0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c,
    +	0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61,
    +	0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f,
    +	0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42,
    +	0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65,
    +	0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
    +	0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18,
    +	0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65,
    +	0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73,
    +	0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73,
    +	0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a,
    +	0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65,
    +	0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e,
    +	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a,
    +	0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24,
    +	0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18,
    +	0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c,
    +	0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    +	0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    +	0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66,
    +	0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65,
    +	0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d,
    +	0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72,
    +	0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70,
    +	0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c,
    +	0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75,
    +	0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75,
    +	0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e,
    +	0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e,
    +	0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f,
    +	0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55,
    +	0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65,
    +	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
    +	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75,
    +	0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
    +	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
    +	0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
    +	0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75,
    +	0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74,
    +	0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65,
    +	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65,
    +	0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f,
    +	0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77,
    +	0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xee, 0x03,
    +	0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    +	0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61,
    +	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
    +	0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63,
    +	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67,
    +	0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46,
    +	0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64,
    +	0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68,
    +	0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e,
    +	0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e,
    +	0x55, 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65,
    +	0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65,
    +	0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72,
    +	0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03,
    +	0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12,
    +	0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f,
    +	0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73,
    +	0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12,
    +	0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f,
    +	0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65,
    +	0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72,
    +	0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18,
    +	0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73,
    +	0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    +	0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50,
    +	0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73,
    +	0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
    +	0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
    +	0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde,
    +	0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02,
    +	0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06,
    +	0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75,
    +	0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74,
    +	0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74,
    +	0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74,
    +	0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 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, 0x10, 0x63, 0x6f, 0x6e, 0x6e,
    +	0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07,
    +	0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72,
    +	0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49,
     	0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18,
    -	0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65,
    -	0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a,
    -	0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
    -	0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e,
    -	0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61,
    -	0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    -	0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64,
    -	0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20,
    -	0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61,
    -	0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    -	0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64,
    -	0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 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, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73,
    -	0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68,
    -	0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d,
    -	0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a,
    -	0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07,
    -	0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    -	0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28,
    -	0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62,
    -	0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18,
    -	0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12,
    -	0x33, 0x0a, 0x07, 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, 0x07, 0x6c, 0x61, 0x74,
    -	0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64,
    -	0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61,
    -	0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63,
    -	0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49,
    -	0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70,
    -	0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62,
    -	0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74,
    -	0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65,
    -	0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a,
    -	0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
    -	0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e,
    -	0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73,
    -	0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a,
    -	0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73,
    -	0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65,
    -	0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12,
    -	0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
    -	0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53,
    -	0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
    +	0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43,
    +	0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16,
    +	0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61,
    +	0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65,
    +	0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65,
    +	0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61,
    +	0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64,
    +	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63,
    +	0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e,
    +	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65,
    +	0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70,
    +	0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f,
    +	0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e,
    +	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69,
    +	0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65,
    +	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, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
    +	0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72,
    +	0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79,
    +	0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74,
    +	0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18,
    +	0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a,
    +	0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c,
    +	0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70,
    +	0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65,
    +	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65,
    +	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 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, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72,
    +	0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22,
    +	0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
    +	0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65,
    +	0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20,
    +	0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72,
    +	0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65,
    +	0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
    +	0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
    +	0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28,
    +	0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d,
    +	0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
    +	0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65,
    +	0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
     	0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09,
     	0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
     	0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72,
     	0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
    -	0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
    -	0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63,
    -	0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c,
    -	0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61,
    -	0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76,
    -	0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a,
    -	0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a,
    -	0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
    -	0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69,
    -	0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
    -	0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
    -	0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
    -	0x72, 0x22, 0xff, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
    -	0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73,
    -	0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f,
    -	0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61,
    -	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61,
    -	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65,
    -	0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65,
    -	0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20,
    -	0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c,
    -	0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12,
    -	0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06,
    -	0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53,
    -	0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53,
    -	0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73,
    -	0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65,
    -	0x6e, 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    -	0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69,
    -	0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
    -	0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53,
    -	0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49,
    -	0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03,
    -	0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18,
    -	0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73,
    -	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69,
    -	0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
    -	0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
    -	0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44,
    -	0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
    -	0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
    -	0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20,
    -	0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b,
    -	0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28,
    -	0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    -	0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e,
    -	0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73,
    -	0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45,
    -	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
    -	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49,
    -	0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
    -	0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52,
    -	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d,
    -	0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79,
    -	0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a,
    -	0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
    -	0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13,
    -	0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f,
    -	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a,
    -	0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70,
    -	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67,
    -	0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12,
    -	0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65,
    -	0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65,
    -	0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    -	0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
    -	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13,
    -	0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
    -	0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65,
    -	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61,
    -	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73,
    -	0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e,
    -	0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65,
    -	0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28,
    -	0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53,
    -	0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e,
    -	0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01,
    -	0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61,
    -	0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
    -	0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18,
    -	0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65,
    -	0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    -	0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61,
    -	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74,
    -	0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e,
    -	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74,
    -	0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65,
    -	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e,
    -	0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63,
    -	0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50,
    -	0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72,
    -	0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a,
    -	0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12,
    -	0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72,
    -	0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65,
    -	0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72,
    -	0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75,
    -	0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
    -	0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64,
    -	0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08,
    -	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
    -	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72,
    -	0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73,
    -	0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73,
    -	0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20,
    -	0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    -	0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f,
    -	0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69,
    -	0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18,
    -	0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54,
    -	0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c,
    -	0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74,
    -	0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d,
    -	0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70,
    -	0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69,
    -	0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74,
    -	0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d,
    -	0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f,
    -	0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74,
    -	0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
    -	0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
    -	0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66,
    -	0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c,
    -	0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61,
    -	0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42,
    -	0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64,
    -	0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50,
    -	0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
    -	0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
    -	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67,
    -	0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e,
    -	0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f,
    -	0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72,
    -	0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x87, 0x04, 0x0a, 0x0b, 0x53,
    -	0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
    -	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65,
    -	0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e,
    -	0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65,
    -	0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65,
    -	0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18,
    -	0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72,
    -	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75,
    -	0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69,
    -	0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 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, 0x74, 0x69, 0x6d, 0x65, 0x73,
    -	0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
    -	0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61,
    -	0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
    -	0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45,
    -	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 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, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04,
    -	0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e,
    -	0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c,
    -	0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x08,
    -	0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57,
    -	0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12,
    -	0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e,
    -	0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49,
    -	0x54, 0x59, 0x10, 0x03, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74,
    -	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45,
    -	0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a,
    -	0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e,
    +	0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10,
    +	0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49,
    +	0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20,
    +	0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14,
    +	0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65,
    +	0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53,
    +	0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
    +	0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18,
    +	0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52,
    +	0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62,
    +	0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
    +	0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xff, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c,
    +	0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
    +	0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    +	0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69,
    +	0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
    +	0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53,
    +	0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
    +	0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65,
    +	0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65,
    +	0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06,
    +	0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65,
    +	0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2b, 0x0a,
    +	0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e,
     	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65,
    -	0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f,
    -	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57,
    -	0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09,
    -	0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52,
    -	0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08,
    -	0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55,
    -	0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xe7,
    -	0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
    -	0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    -	0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74,
    -	0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57,
    -	0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
    -	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    -	0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    -	0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    -	0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f,
    -	0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74,
    -	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c,
    -	0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e,
    -	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    -	0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52,
    -	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65,
    -	0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    -	0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a,
    -	0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c,
    -	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73,
    -	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f,
    -	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c,
    -	0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
    -	0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
    -	0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67,
    -	0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65,
    -	0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c,
    -	0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69,
    +	0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69,
    +	0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    +	0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
    +	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75,
    +	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74,
    +	0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e,
    +	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
    +	0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61,
    +	0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70,
    +	0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
    +	0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e,
    +	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
    +	0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73,
    +	0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07,
    +	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a,
    +	0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52,
    +	0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d,
    +	0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61,
    +	0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49,
    +	0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76,
    +	0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f,
    +	0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c,
    +	0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
    +	0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a,
    +	0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61,
    +	0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67,
    +	0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a,
    +	0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
    +	0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61,
    +	0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66,
    +	0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49,
    +	0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64,
    +	0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61,
    +	0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14,
    +	0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71,
    +	0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65,
    +	0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c,
    +	0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65,
    +	0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65,
    +	0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76,
    +	0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76,
    +	0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65,
    +	0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
    +	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69,
     	0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    -	0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65,
    -	0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53,
    -	0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
    -	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c,
    -	0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65,
    -	0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    -	0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12,
    -	0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77,
    +	0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
    +	0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
    +	0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e,
    +	0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a,
    +	0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61,
    +	0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a,
    +	0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73,
    +	0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65,
    +	0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65,
    +	0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
    +	0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12,
    +	0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c,
    +	0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65,
    +	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65,
    +	0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05,
    +	0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22,
    +	0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70,
    +	0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20,
    +	0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20,
    +	0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72,
    +	0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    +	0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03,
    +	0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10,
    +	0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b,
    +	0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66,
    +	0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52,
    +	0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28,
    +	0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20,
    +	0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61,
    +	0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
    +	0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e,
    +	0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f,
    +	0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18,
    +	0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
    +	0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04,
    +	0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74,
    +	0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
    +	0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74,
    +	0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64,
    +	0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
    +	0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70,
    +	0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00,
    +	0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a,
    +	0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d,
    +	0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12,
    +	0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01,
    +	0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01,
    +	0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42,
    +	0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a,
    +	0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a,
    +	0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
    +	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18,
    +	0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f,
    +	0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77,
    +	0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67,
    +	0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00,
    +	0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61,
    +	0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61,
    +	0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a,
    +	0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01,
    +	0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72,
    +	0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73,
    +	0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73,
    +	0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e,
    +	0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a,
    +	0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    +	0x74, 0x22, 0x87, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e,
    +	0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
    +	0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20,
    +	0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73,
    +	0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74,
    +	0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63,
    +	0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65,
    +	0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74,
    +	0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
    +	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12,
    +	0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
    +	0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06,
    +	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, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d,
    +	0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65,
    +	0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79,
    +	0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65,
    +	0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
    +	0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 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, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72,
    +	0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a,
    +	0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52,
    +	0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41,
    +	0x4c, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12,
    +	0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03,
    +	0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54,
    +	0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e,
    +	0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x22, 0x12, 0x0a, 0x10, 0x47,
    +	0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
    +	0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01,
    +	0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79,
    +	0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74,
    +	0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a,
    +	0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41,
    +	0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02,
    +	0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57,
    +	0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12,
    +	0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52,
    +	0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xe7, 0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    +	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
    +	0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
    +	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    +	0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12,
    +	0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f,
    +	0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67,
    +	0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02,
    +	0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65,
    +	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55,
    +	0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13,
    +	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75,
    +	0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77,
    +	0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47,
    +	0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43,
    +	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    +	0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12,
    +	0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74,
    +	0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e,
    +	0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d,
    +	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65,
    +	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74,
    +	0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    +	0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    +	0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c,
    +	0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65,
    +	0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e,
    +	0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62,
    +	0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    +	0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75,
    +	0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48,
    +	0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76,
    +	0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65,
    +	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c,
    +	0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    +	0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75,
    +	0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74,
    +	0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    +	0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73,
    +	0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52,
    +	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65,
    +	0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    +	0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61,
    +	0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
    +	0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12,
    +	0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53,
    +	0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65,
    +	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65,
    +	0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69,
    +	0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72,
    +	0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    +	0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77,
     	0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63,
    -	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50,
    -	0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63,
    -	0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61,
    -	0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    -	0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61,
    -	0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44,
    -	0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74,
    -	0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63,
    -	0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74,
    -	0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74,
    -	0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76,
    -	0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f,
    -	0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
    +	0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54,
    +	0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65,
    +	0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52,
    +	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69,
    +	0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74,
    +	0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47,
    +	0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45,
    +	0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42,
    +	0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
    +	0x33,
     }
     
     var (
    diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto
    index 92a289c41..012b8b4db 100644
    --- a/client/proto/daemon.proto
    +++ b/client/proto/daemon.proto
    @@ -122,6 +122,14 @@ message LoginRequest {
       optional bool block_lan_access = 24;
     
       optional bool disable_notifications = 25;
    +
    +  repeated string dns_labels = 26;
    +
    +  // cleanDNSLabels clean map list of DNS labels.
    +  // This is needed because the generated code
    +  // omits initialized empty slices due to omitempty tags
    +  bool cleanDNSLabels = 27;
    +
     }
     
     message LoginResponse {
    diff --git a/client/server/server.go b/client/server/server.go
    index 9250b3e8b..e4e2c8f6f 100644
    --- a/client/server/server.go
    +++ b/client/server/server.go
    @@ -22,6 +22,7 @@ import (
     
     	"github.com/netbirdio/netbird/client/internal/auth"
     	"github.com/netbirdio/netbird/client/system"
    +	"github.com/netbirdio/netbird/management/domain"
     
     	"github.com/netbirdio/netbird/client/internal"
     	"github.com/netbirdio/netbird/client/internal/peer"
    @@ -404,6 +405,15 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
     		s.latestConfigInput.BlockLANAccess = msg.BlockLanAccess
     	}
     
    +	if msg.CleanDNSLabels {
    +		inputConfig.DNSLabels = domain.List{}
    +		s.latestConfigInput.DNSLabels = nil
    +	} else if msg.DnsLabels != nil {
    +		dnsLabels := domain.FromPunycodeList(msg.DnsLabels)
    +		inputConfig.DNSLabels = dnsLabels
    +		s.latestConfigInput.DNSLabels = dnsLabels
    +	}
    +
     	if msg.DisableNotifications != nil {
     		inputConfig.DisableNotifications = msg.DisableNotifications
     		s.latestConfigInput.DisableNotifications = msg.DisableNotifications
    diff --git a/management/client/client.go b/management/client/client.go
    index e79884292..e9eeaccc1 100644
    --- a/management/client/client.go
    +++ b/management/client/client.go
    @@ -7,6 +7,7 @@ import (
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/client/system"
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/proto"
     )
     
    @@ -15,7 +16,7 @@ type Client interface {
     	Sync(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error
     	GetServerPublicKey() (*wgtypes.Key, error)
     	Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error)
    -	Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error)
    +	Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
     	GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
     	GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error)
     	GetNetworkMap(sysInfo *system.Info) (*proto.NetworkMap, error)
    diff --git a/management/client/client_test.go b/management/client/client_test.go
    index b4ee58298..6ef5df163 100644
    --- a/management/client/client_test.go
    +++ b/management/client/client_test.go
    @@ -177,7 +177,7 @@ func TestClient_LoginUnregistered_ShouldThrow_401(t *testing.T) {
     		t.Fatal(err)
     	}
     	sysInfo := system.GetInfo(context.TODO())
    -	_, err = client.Login(*key, sysInfo, nil)
    +	_, err = client.Login(*key, sysInfo, nil, nil)
     	if err == nil {
     		t.Error("expecting err on unregistered login, got nil")
     	}
    diff --git a/management/client/grpc.go b/management/client/grpc.go
    index 53f66da18..d02509c27 100644
    --- a/management/client/grpc.go
    +++ b/management/client/grpc.go
    @@ -19,6 +19,7 @@ import (
     
     	"github.com/netbirdio/netbird/client/system"
     	"github.com/netbirdio/netbird/encryption"
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/proto"
     	nbgrpc "github.com/netbirdio/netbird/util/grpc"
     )
    @@ -373,12 +374,12 @@ func (c *GrpcClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken s
     }
     
     // Login attempts login to Management Server. Takes care of encrypting and decrypting messages.
    -func (c *GrpcClient) Login(serverKey wgtypes.Key, sysInfo *system.Info, pubSSHKey []byte) (*proto.LoginResponse, error) {
    +func (c *GrpcClient) Login(serverKey wgtypes.Key, sysInfo *system.Info, pubSSHKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) {
     	keys := &proto.PeerKeys{
     		SshPubKey: pubSSHKey,
     		WgPubKey:  []byte(c.key.PublicKey().String()),
     	}
    -	return c.login(serverKey, &proto.LoginRequest{Meta: infoToMetaData(sysInfo), PeerKeys: keys})
    +	return c.login(serverKey, &proto.LoginRequest{Meta: infoToMetaData(sysInfo), PeerKeys: keys, DnsLabels: dnsLabels.ToPunycodeList()})
     }
     
     // GetDeviceAuthorizationFlow returns a device authorization flow information.
    diff --git a/management/client/mock.go b/management/client/mock.go
    index 73a7ac38f..11564093a 100644
    --- a/management/client/mock.go
    +++ b/management/client/mock.go
    @@ -6,6 +6,7 @@ import (
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/client/system"
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/proto"
     )
     
    @@ -14,7 +15,7 @@ type MockClient struct {
     	SyncFunc                       func(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error
     	GetServerPublicKeyFunc         func() (*wgtypes.Key, error)
     	RegisterFunc                   func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error)
    -	LoginFunc                      func(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error)
    +	LoginFunc                      func(serverKey wgtypes.Key, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
     	GetDeviceAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
     	GetPKCEAuthorizationFlowFunc   func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error)
     	SyncMetaFunc                   func(sysInfo *system.Info) error
    @@ -52,11 +53,11 @@ func (m *MockClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken s
     	return m.RegisterFunc(serverKey, setupKey, jwtToken, info, sshKey)
     }
     
    -func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) {
    +func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) {
     	if m.LoginFunc == nil {
     		return nil, nil
     	}
    -	return m.LoginFunc(serverKey, info, sshKey)
    +	return m.LoginFunc(serverKey, info, sshKey, dnsLabels)
     }
     
     func (m *MockClient) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) {
    diff --git a/management/domain/validate.go b/management/domain/validate.go
    new file mode 100644
    index 000000000..bcbf26e05
    --- /dev/null
    +++ b/management/domain/validate.go
    @@ -0,0 +1,65 @@
    +package domain
    +
    +import (
    +	"fmt"
    +	"regexp"
    +	"strings"
    +)
    +
    +const maxDomains = 32
    +
    +// ValidateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
    +func ValidateDomains(domains []string) (List, error) {
    +	if len(domains) == 0 {
    +		return nil, fmt.Errorf("domains list is empty")
    +	}
    +	if len(domains) > maxDomains {
    +		return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
    +	}
    +
    +	domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
    +
    +	var domainList List
    +
    +	for _, d := range domains {
    +		d := strings.ToLower(d)
    +
    +		// handles length and idna conversion
    +		punycode, err := FromString(d)
    +		if err != nil {
    +			return domainList, fmt.Errorf("convert domain to punycode: %s: %w", d, err)
    +		}
    +
    +		if !domainRegex.MatchString(string(punycode)) {
    +			return domainList, fmt.Errorf("invalid domain format: %s", d)
    +		}
    +
    +		domainList = append(domainList, punycode)
    +	}
    +	return domainList, nil
    +}
    +
    +// ValidateDomainsStrSlice checks if each domain in the list is valid
    +func ValidateDomainsStrSlice(domains []string) ([]string, error) {
    +	if len(domains) == 0 {
    +		return nil, nil
    +	}
    +	if len(domains) > maxDomains {
    +		return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
    +	}
    +
    +	domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
    +
    +	var domainList []string
    +
    +	for _, d := range domains {
    +		d := strings.ToLower(d)
    +
    +		if !domainRegex.MatchString(d) {
    +			return domainList, fmt.Errorf("invalid domain format: %s", d)
    +		}
    +
    +		domainList = append(domainList, d)
    +	}
    +	return domainList, nil
    +}
    diff --git a/management/domain/validate_test.go b/management/domain/validate_test.go
    new file mode 100644
    index 000000000..c9c042d9d
    --- /dev/null
    +++ b/management/domain/validate_test.go
    @@ -0,0 +1,206 @@
    +package domain
    +
    +import (
    +	"fmt"
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +)
    +
    +func TestValidateDomains(t *testing.T) {
    +	tests := []struct {
    +		name     string
    +		domains  []string
    +		expected List
    +		wantErr  bool
    +	}{
    +		{
    +			name:     "Empty list",
    +			domains:  nil,
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Valid ASCII domain",
    +			domains:  []string{"sub.ex-ample.com"},
    +			expected: List{"sub.ex-ample.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Valid Unicode domain",
    +			domains:  []string{"münchen.de"},
    +			expected: List{"xn--mnchen-3ya.de"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Valid Unicode, all labels",
    +			domains:  []string{"中国.中国.中国"},
    +			expected: List{"xn--fiqs8s.xn--fiqs8s.xn--fiqs8s"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "With underscores",
    +			domains:  []string{"_jabber._tcp.gmail.com"},
    +			expected: List{"_jabber._tcp.gmail.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Invalid domain format",
    +			domains:  []string{"-example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Invalid domain format 2",
    +			domains:  []string{"example.com-"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Multiple domains valid and invalid",
    +			domains:  []string{"google.com", "invalid,nbdomain.com", "münchen.de"},
    +			expected: List{"google.com"},
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Valid wildcard domain",
    +			domains:  []string{"*.example.com"},
    +			expected: List{"*.example.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Wildcard with dot domain",
    +			domains:  []string{".*.example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Wildcard with dot domain",
    +			domains:  []string{".*.example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Invalid wildcard domain",
    +			domains:  []string{"a.*.example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			got, err := ValidateDomains(tt.domains)
    +			assert.Equal(t, tt.wantErr, err != nil)
    +			assert.Equal(t, got, tt.expected)
    +		})
    +	}
    +}
    +
    +// TestValidateDomainsStrSlice tests the ValidateDomainsStrSlice function.
    +func TestValidateDomainsStrSlice(t *testing.T) {
    +	// Generate a slice of valid domains up to maxDomains
    +	validDomains := make([]string, maxDomains)
    +	for i := 0; i < maxDomains; i++ {
    +		validDomains[i] = fmt.Sprintf("example%d.com", i)
    +	}
    +
    +	tests := []struct {
    +		name     string
    +		domains  []string
    +		expected []string
    +		wantErr  bool
    +	}{
    +		{
    +			name:     "Empty list",
    +			domains:  nil,
    +			expected: nil,
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Single valid ASCII domain",
    +			domains:  []string{"sub.ex-ample.com"},
    +			expected: []string{"sub.ex-ample.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Underscores in labels",
    +			domains:  []string{"_jabber._tcp.gmail.com"},
    +			expected: []string{"_jabber._tcp.gmail.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			// Unlike ValidateDomains (which converts to punycode),
    +			// ValidateDomainsStrSlice will fail on non-ASCII domain chars.
    +			name:     "Unicode domain fails (no punycode conversion)",
    +			domains:  []string{"münchen.de"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Invalid domain format - leading dash",
    +			domains:  []string{"-example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Invalid domain format - trailing dash",
    +			domains:  []string{"example-.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			// The function stops on the first invalid domain and returns an error,
    +			// so only the first domain is definitely valid, but the second is invalid.
    +			name:     "Multiple domains with a valid one, then invalid",
    +			domains:  []string{"google.com", "invalid_domain.com-"},
    +			expected: []string{"google.com"},
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Valid wildcard domain",
    +			domains:  []string{"*.example.com"},
    +			expected: []string{"*.example.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Wildcard with leading dot - invalid",
    +			domains:  []string{".*.example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Invalid wildcard with multiple asterisks",
    +			domains:  []string{"a.*.example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Exactly maxDomains items (valid)",
    +			domains:  validDomains,
    +			expected: validDomains,
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Exceeds maxDomains items",
    +			domains:  append(validDomains, "extra.com"),
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			got, err := ValidateDomainsStrSlice(tt.domains)
    +			// Check if we got an error where expected
    +			if tt.wantErr {
    +				assert.Error(t, err)
    +			} else {
    +				assert.NoError(t, err)
    +			}
    +
    +			// Compare the returned domains to what we expect
    +			assert.Equal(t, tt.expected, got)
    +		})
    +	}
    +}
    diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go
    index a654a6365..2cd00783e 100644
    --- a/management/proto/management.pb.go
    +++ b/management/proto/management.pb.go
    @@ -537,7 +537,8 @@ type LoginRequest struct {
     	// SSO token (can be empty)
     	JwtToken string `protobuf:"bytes,3,opt,name=jwtToken,proto3" json:"jwtToken,omitempty"`
     	// Can be absent for now.
    -	PeerKeys *PeerKeys `protobuf:"bytes,4,opt,name=peerKeys,proto3" json:"peerKeys,omitempty"`
    +	PeerKeys  *PeerKeys `protobuf:"bytes,4,opt,name=peerKeys,proto3" json:"peerKeys,omitempty"`
    +	DnsLabels []string  `protobuf:"bytes,5,rep,name=dnsLabels,proto3" json:"dnsLabels,omitempty"`
     }
     
     func (x *LoginRequest) Reset() {
    @@ -600,6 +601,13 @@ func (x *LoginRequest) GetPeerKeys() *PeerKeys {
     	return nil
     }
     
    +func (x *LoginRequest) GetDnsLabels() []string {
    +	if x != nil {
    +		return x.DnsLabels
    +	}
    +	return nil
    +}
    +
     // PeerKeys is additional peer info like SSH pub key and WireGuard public key.
     // This message is sent on Login or register requests, or when a key rotation has to happen.
     type PeerKeys struct {
    @@ -3093,7 +3101,7 @@ var file_management_proto_rawDesc = []byte{
     	0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a,
     	0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61,
     	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73,
    -	0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xa8, 0x01,
    +	0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xc6, 0x01,
     	0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a,
     	0x0a, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
     	0x52, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65,
    @@ -3104,402 +3112,404 @@ var file_management_proto_rawDesc = []byte{
     	0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65,
     	0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
     	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x08,
    -	0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72,
    -	0x4b, 0x65, 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65,
    -	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b,
    -	0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f,
    -	0x0a, 0x0b, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a,
    -	0x05, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c,
    -	0x6f, 0x75, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18,
    -	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22,
    -	0x5c, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18,
    -	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65,
    -	0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73,
    -	0x74, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75,
    -	0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f,
    -	0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x02,
    -	0x0a, 0x05, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    -	0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62,
    -	0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73,
    -	0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
    -	0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69,
    -	0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53,
    -	0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52,
    -	0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65,
    -	0x64, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65,
    -	0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13,
    -	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75,
    -	0x74, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65,
    -	0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08,
    -	0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52,
    -	0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
    -	0x44, 0x4e, 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62,
    -	0x6c, 0x65, 0x44, 0x4e, 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
    -	0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f,
    -	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22,
    -	0xf2, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65,
    -	0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12,
    -	0x0a, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f,
    -	0x4f, 0x53, 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f,
    -	0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a,
    -	0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53,
    -	0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65,
    -	0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69,
    -	0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
    -	0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
    -	0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
    -	0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56,
    -	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73,
    -	0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72,
    -	0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41,
    -	0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a,
    -	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f,
    -	0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18,
    -	0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c,
    -	0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f,
    -	0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e,
    -	0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28,
    -	0x0a, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65,
    -	0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75,
    -	0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69,
    -	0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e,
    -	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72,
    -	0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d,
    -	0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03,
    -	0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
    -	0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66,
    -	0x6c, 0x61, 0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e,
    -	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66,
    -	0x6c, 0x61, 0x67, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72,
    -	0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
    -	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69,
    -	0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72,
    -	0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43,
    -	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61,
    -	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
    -	0x2a, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
    -	0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65,
    -	0x63, 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53,
    -	0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    -	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
    -	0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 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, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07,
    -	0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76,
    -	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22,
    -	0xd3, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    -	0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
    -	0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f,
    -	0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12,
    -	0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f,
    -	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74,
    -	0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
    -	0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    -	0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06,
    -	0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18,
    -	0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    -	0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05,
    -	0x72, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f,
    -	0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
    -	0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x4c,
    +	0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73,
    +	0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65,
    +	0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
    +	0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79,
    +	0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
    +	0x28, 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x0b,
    +	0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63,
    +	0x6c, 0x6f, 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75,
    +	0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x5c, 0x0a,
    +	0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x78, 0x69,
    +	0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12,
    +	0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e,
    +	0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65,
    +	0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x02, 0x0a, 0x05,
    +	0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61,
    +	0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
    +	0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65,
    +	0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65,
    +	0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13,
    +	0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
    +	0x69, 0x76, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48,
    +	0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73,
    +	0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12,
    +	0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
    +	0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69,
    +	0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65,
    +	0x73, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76,
    +	0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13,
    +	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75,
    +	0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x4e,
    +	0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
    +	0x44, 0x4e, 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69,
    +	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69,
    +	0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22, 0xf2, 0x04,
    +	0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61,
    +	0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04,
    +	0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53,
    +	0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65,
    +	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08,
    +	0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
    +	0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x62,
    +	0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
    +	0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24,
    +	0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
    +	0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72,
    +	0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
    +	0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69,
    +	0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64,
    +	0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d,
    +	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79,
    +	0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0c, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75,
    +	0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75,
    +	0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x79,
    +	0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f,
    +	0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18,
    +	0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61,
    +	0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f,
    +	0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61,
    +	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e,
    +	0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e,
    +	0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b,
    +	0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69,
    +	0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66, 0x6c, 0x61,
    +	0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66, 0x6c, 0x61,
    +	0x67, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43,
    +	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61,
    +	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43,
    +	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e,
    +	0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    +	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a,
    +	0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
    +	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b,
    +	0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72,
    +	0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10,
    +	0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
    +	0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 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,
    +	0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65,
    +	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72,
    +	0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xd3, 0x01,
    +	0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
    +	0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16,
    +	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a,
    +	0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d,
    +	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63,
    +	0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74,
    +	0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03,
    +	0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    +	0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69,
    +	0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20,
    +	0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    +	0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65,
    +	0x6c, 0x61, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
    +	0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    +	0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50,
    +	0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
    +	0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a,
    +	0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12,
    +	0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54,
    +	0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d,
    +	0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a,
    +	0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c,
    +	0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61,
    +	0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61,
    +	0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69,
    +	0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74,
    +	0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x7d, 0x0a,
    +	0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f,
    +	0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
    +	0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
     	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
    -	0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f,
    -	0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
    -	0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10,
    -	0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48,
    -	0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04,
    -	0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
    -	0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75,
    -	0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c,
    -	0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
    -	0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
    -	0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22,
    -	0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74,
    -	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f,
    -	0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e,
    -	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
    -	0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12,
    -	0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73,
    -	0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb,
    -	0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a,
    -	0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
    -	0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68,
    -	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d,
    -	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12,
    -	0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71,
    -	0x64, 0x6e, 0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65,
    -	0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e,
    -	0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75,
    -	0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c,
    -	0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xf3, 0x04, 0x0a,
    -	0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53,
    -	0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72,
    -	0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    -	0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
    -	0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72,
    -	0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
    -	0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65,
    -	0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b,
    -	0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72,
    -	0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74,
    -	0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50,
    -	0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52,
    -	0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61,
    -	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06,
    -	0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    -	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
    -	0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f,
    -	0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
    -	0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52,
    -	0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
    -	0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a,
    -	0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08,
    -	0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    -	0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d,
    -	0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a,
    -	0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73,
    -	0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72,
    -	0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74,
    -	0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77,
    -	0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d,
    -	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74,
    -	0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72,
    -	0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c,
    -	0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65,
    +	0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04,
    +	0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72,
    +	0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a,
    +	0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61,
    +	0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64,
    +	0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f,
    +	0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e,
    +	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04,
    +	0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e,
    +	0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44,
    +	0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62,
    +	0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69,
    +	0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74,
    +	0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xf3, 0x04, 0x0a, 0x0a, 0x4e,
    +	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72,
    +	0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61,
    +	0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70,
    +	0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d,
    +	0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c,
    +	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f,
    +	0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65,
    +	0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d,
    +	0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18,
    +	0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65,
    +	0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75,
    +	0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    +	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f,
    +	0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    +	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09,
    +	0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66,
    +	0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32,
    +	0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d,
    +	0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f,
    +	0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46,
    +	0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03,
    +	0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
    +	0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69,
    +	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66,
    +	0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d,
    +	0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77,
    +	0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
    +	0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c,
    +	0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d,
    +	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46,
    +	0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75,
    +	0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73,
    +	0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61,
    +	0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65,
     	0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79,
    -	0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69,
    -	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70,
    -	0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65,
    -	0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62,
    -	0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62,
    -	0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70,
    -	0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64,
    -	0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    -	0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73,
    -	0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e,
    -	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09,
    -	0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68,
    -	0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73,
    -	0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68,
    -	0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73,
    -	0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63,
    -	0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
    -	0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65,
    -	0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
    -	0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
    -	0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f,
    -	0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f,
    -	0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12,
    -	0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    -	0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12,
    -	0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50,
    -	0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    -	0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50,
    -	0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    -	0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
    -	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d,
    -	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64,
    -	0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64,
    -	0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f,
    -	0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43,
    -	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43,
    -	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e,
    -	0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43,
    -	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44,
    -	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d,
    -	0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18,
    -	0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12,
    -	0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64,
    -	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76,
    -	0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    -	0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
    -	0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64,
    -	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55,
    -	0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52,
    -	0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41,
    -	0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70,
    -	0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68,
    -	0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
    -	0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c,
    -	0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63,
    -	0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12,
    -	0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12,
    -	0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74,
    -	0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
    -	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50,
    -	0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12,
    -	0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52,
    -	0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75,
    -	0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73,
    -	0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44,
    -	0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a,
    -	0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
    -	0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52,
    -	0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70,
    -	0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e,
    -	0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76,
    -	0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d,
    -	0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20,
    -	0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    -	0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70,
    -	0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75,
    -	0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65,
    -	0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52,
    -	0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a,
    -	0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f,
    -	0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61,
    -	0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20,
    -	0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    -	0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52,
    -	0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65,
    -	0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79,
    -	0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14,
    -	0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43,
    -	0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28,
    -	0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18,
    -	0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a,
    -	0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70,
    -	0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
    -	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    -	0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e,
    -	0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72,
    -	0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69,
    -	0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18,
    -	0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32,
    -	0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45,
    -	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65,
    -	0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c,
    -	0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
    -	0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50,
    -	0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
    -	0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8b, 0x02, 0x0a,
    -	0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a,
    -	0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50,
    -	0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69,
    -	0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    -	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74,
    -	0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e,
    -	0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16,
    +	0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43,
    +	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65,
    +	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65,
    +	0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18,
    +	0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70,
    +	0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03,
    +	0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    +	0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53,
    +	0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e,
    +	0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68,
    +	0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75,
    +	0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50,
    +	0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41,
    +	0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77,
    +	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69,
    +	0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46,
    +	0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18,
    +	0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69,
    +	0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
    +	0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a,
    +	0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a,
    +	0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43,
    +	0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
    +	0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43,
    +	0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
    +	0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f,
    +	0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e,
    +	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69,
    +	0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69,
    +	0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69,
    +	0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53,
    +	0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69,
    +	0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d,
    +	0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69,
    +	0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a,
    +	0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f,
    +	0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63,
    +	0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a,
    +	0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f,
    +	0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65,
    +	0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55,
    +	0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74,
    +	0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69,
    +	0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72,
    +	0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    +	0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18,
    +	0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55,
    +	0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a,
    +	0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a,
    +	0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
    +	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    +	0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65,
    +	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65,
    +	0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a,
    +	0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d,
    +	0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72,
    +	0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75,
    +	0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44,
    +	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f,
    +	0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75,
    +	0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f,
    +	0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62,
    +	0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
    +	0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53,
    +	0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
    +	0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e,
    +	0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10,
    +	0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73,
    +	0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18,
    +	0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43,
    +	0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75,
    +	0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61,
    +	0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
    +	0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
    +	0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53,
    +	0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63,
    +	0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65,
    +	0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05,
    +	0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61,
    +	0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
    +	0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e,
    +	0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38,
    +	0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20,
    +	0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    +	0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d,
    +	0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d,
    +	0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61,
    +	0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20,
    +	0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14,
    +	0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61,
    +	0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72,
    +	0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
    +	0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e,
    +	0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16,
    +	0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06,
    +	0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03,
    +	0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8b, 0x02, 0x0a, 0x0c, 0x46,
    +	0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50,
    +	0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65,
    +	0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    +	0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f,
    +	0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06,
    +	0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d,
    +	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63,
    +	0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08,
    +	0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18,
     	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65,
    -	0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34,
    -	0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e,
    -	0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75,
    -	0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74,
    -	0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74,
    -	0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e,
    -	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f,
    -	0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65,
    -	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05,
    -	0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74,
    -	0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14,
    -	0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46,
    -	0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66,
    -	0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48,
    -	0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65,
    -	0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    -	0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e,
    -	0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52,
    -	0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e,
    -	0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d,
    -	0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xd1, 0x02,
    -	0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52,
    -	0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e,
    -	0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63,
    -	0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f,
    -	0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
    -	0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69,
    -	0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65,
    -	0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f,
    -	0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61,
    -	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f,
    -	0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
    -	0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28,
    -	0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50,
    -	0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66,
    -	0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12,
    -	0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09,
    -	0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73,
    -	0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28,
    -	0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
    -	0x6c, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
    -	0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07,
    -	0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02,
    -	0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d,
    -	0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a,
    -	0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
    -	0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10,
    -	0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12,
    -	0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44,
    -	0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c,
    -	0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    -	0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61,
    -	0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
    +	0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
    +	0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e,
    +	0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08,
    +	0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65,
    +	0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50,
    +	0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d,
    +	0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05,
    +	0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c,
    +	0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12,
    +	0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52,
    +	0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    +	0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65,
    +	0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e,
    +	0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f,
    +	0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xd1, 0x02, 0x0a, 0x11,
    +	0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c,
    +	0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65,
    +	0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52,
    +	0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61,
    +	0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
    +	0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74,
    +	0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f,
    +	0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    +	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f,
    +	0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a,
    +	0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32,
    +	0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72,
    +	0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12,
    +	0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a,
    +	0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
    +	0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f,
    +	0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52,
    +	0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2a,
    +	0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
    +	0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03,
    +	0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07,
    +	0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10,
    +	0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a,
    +	0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06,
    +	0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a,
    +	0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a,
    +	0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f,
    +	0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67,
    +	0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
     	0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
    -	0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e,
    -	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
    -	0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e,
    +	0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00,
    +	0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
     	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d,
    -	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65,
    -	0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e,
    -	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e,
    -	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65,
    -	0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33,
    -	0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61,
    -	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11,
    -	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74,
    -	0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65,
    -	0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f,
    -	0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
    -	0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a,
    +	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    +	0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73,
    +	0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53,
    +	0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61,
    +	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b,
    +	0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09,
    +	0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    +	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d,
    +	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22,
    +	0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75,
    +	0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12,
     	0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63,
    -	0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12,
    -	0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72,
    -	0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61,
    -	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
    -	0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    +	0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e,
    +	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79,
    +	0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a,
    +	0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
    +	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
     	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
    -	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e,
    -	0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    -	0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73,
    -	0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    -	0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f,
    -	0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
    +	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    +	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65,
    +	0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d,
    +	0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    +	0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
    +	0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
    +	0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
    +	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
     }
     
     var (
    diff --git a/management/proto/management.proto b/management/proto/management.proto
    index b75d3f956..cd207136f 100644
    --- a/management/proto/management.proto
    +++ b/management/proto/management.proto
    @@ -97,7 +97,8 @@ message LoginRequest {
       string jwtToken = 3;
       // Can be absent for now.
       PeerKeys peerKeys = 4;
    -
    +  
    +  repeated string dnsLabels = 5;
     }
     
     // PeerKeys is additional peer info like SSH pub key and WireGuard public key.
    diff --git a/management/server/account.go b/management/server/account.go
    index a0c6fd0b0..661569418 100644
    --- a/management/server/account.go
    +++ b/management/server/account.go
    @@ -63,7 +63,7 @@ type AccountManager interface {
     	GetOrCreateAccountByUser(ctx context.Context, userId, domain string) (*types.Account, error)
     	GetAccount(ctx context.Context, accountID string) (*types.Account, error)
     	CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType, expiresIn time.Duration,
    -		autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error)
    +		autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error)
     	SaveSetupKey(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error)
     	CreateUser(ctx context.Context, accountID, initiatorUserID string, key *types.UserInfo) (*types.UserInfo, error)
     	DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error
    diff --git a/management/server/account_test.go b/management/server/account_test.go
    index eb36dbd84..7d59544e0 100644
    --- a/management/server/account_test.go
    +++ b/management/server/account_test.go
    @@ -1080,7 +1080,7 @@ func TestAccountManager_AddPeer(t *testing.T) {
     
     	serial := account.Network.CurrentSerial() // should be 0
     
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     		return
    @@ -1456,7 +1456,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
     		t.Fatal(err)
     	}
     
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     		return
    @@ -2948,7 +2948,7 @@ func setupNetworkMapTest(t *testing.T) (*DefaultAccountManager, *types.Account,
     		t.Fatal(err)
     	}
     
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     	}
    diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go
    index e8e0c422e..8f5fae3e4 100644
    --- a/management/server/grpcserver.go
    +++ b/management/server/grpcserver.go
    @@ -481,6 +481,7 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
     		UserID:          userID,
     		SetupKey:        loginReq.GetSetupKey(),
     		ConnectionIP:    realIP,
    +		ExtraDNSLabels:  loginReq.GetDnsLabels(),
     	})
     	if err != nil {
     		log.WithContext(ctx).Warnf("failed logging in peer %s: %s", peerKey, err)
    diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml
    index f53092415..83f45ef91 100644
    --- a/management/server/http/api/openapi.yml
    +++ b/management/server/http/api/openapi.yml
    @@ -361,6 +361,12 @@ components:
                   description: System serial number
                   type: string
                   example: "C02XJ0J0JGH7"
    +            extra_dns_labels:
    +              description: Extra DNS labels added to the peer
    +              type: array
    +              items:
    +                type: string
    +                example: "stage-host-1"
               required:
                 - city_name
                 - connected
    @@ -384,6 +390,7 @@ components:
                 - ui_version
                 - approval_required
                 - serial_number
    +            - extra_dns_labels
         AccessiblePeer:
           allOf:
             - $ref: '#/components/schemas/PeerMinimum'
    @@ -503,6 +510,10 @@ components:
               description: Indicate that the peer will be ephemeral or not
               type: boolean
               example: true
    +        allow_extra_dns_labels:
    +          description: Allow extra DNS labels to be added to the peer
    +          type: boolean
    +          example: true
           required:
             - id
             - key
    @@ -518,6 +529,7 @@ components:
             - updated_at
             - usage_limit
             - ephemeral
    +        - allow_extra_dns_labels
         SetupKeyClear:
           allOf:
             - $ref: '#/components/schemas/SetupKeyBase'
    @@ -587,6 +599,10 @@ components:
               description: Indicate that the peer will be ephemeral or not
               type: boolean
               example: true
    +        allow_extra_dns_labels:
    +          description: Allow extra DNS labels to be added to the peer
    +          type: boolean
    +          example: true
           required:
             - name
             - type
    diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go
    index 943d1b327..eb57d5d66 100644
    --- a/management/server/http/api/types.gen.go
    +++ b/management/server/http/api/types.gen.go
    @@ -297,6 +297,9 @@ type CountryCode = string
     
     // CreateSetupKeyRequest defines model for CreateSetupKeyRequest.
     type CreateSetupKeyRequest struct {
    +	// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
    +	AllowExtraDnsLabels *bool `json:"allow_extra_dns_labels,omitempty"`
    +
     	// AutoGroups List of group IDs to auto-assign to peers registered with this key
     	AutoGroups []string `json:"auto_groups"`
     
    @@ -689,6 +692,9 @@ type Peer struct {
     	// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
     	DnsLabel string `json:"dns_label"`
     
    +	// ExtraDnsLabels Extra DNS labels added to the peer
    +	ExtraDnsLabels []string `json:"extra_dns_labels"`
    +
     	// GeonameId Unique identifier from the GeoNames database for a specific geographical location.
     	GeonameId int `json:"geoname_id"`
     
    @@ -767,6 +773,9 @@ type PeerBatch struct {
     	// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
     	DnsLabel string `json:"dns_label"`
     
    +	// ExtraDnsLabels Extra DNS labels added to the peer
    +	ExtraDnsLabels []string `json:"extra_dns_labels"`
    +
     	// GeonameId Unique identifier from the GeoNames database for a specific geographical location.
     	GeonameId int `json:"geoname_id"`
     
    @@ -1230,6 +1239,9 @@ type RulePortRange struct {
     
     // SetupKey defines model for SetupKey.
     type SetupKey struct {
    +	// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
    +	AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
    +
     	// AutoGroups List of group IDs to auto-assign to peers registered with this key
     	AutoGroups []string `json:"auto_groups"`
     
    @@ -1275,6 +1287,9 @@ type SetupKey struct {
     
     // SetupKeyBase defines model for SetupKeyBase.
     type SetupKeyBase struct {
    +	// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
    +	AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
    +
     	// AutoGroups List of group IDs to auto-assign to peers registered with this key
     	AutoGroups []string `json:"auto_groups"`
     
    @@ -1317,6 +1332,9 @@ type SetupKeyBase struct {
     
     // SetupKeyClear defines model for SetupKeyClear.
     type SetupKeyClear struct {
    +	// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
    +	AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
    +
     	// AutoGroups List of group IDs to auto-assign to peers registered with this key
     	AutoGroups []string `json:"auto_groups"`
     
    diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go
    index cdd8026f2..26153d0a1 100644
    --- a/management/server/http/handlers/peers/peers_handler.go
    +++ b/management/server/http/handlers/peers/peers_handler.go
    @@ -338,6 +338,7 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD
     		UserId:                      peer.UserID,
     		UiVersion:                   peer.Meta.UIVersion,
     		DnsLabel:                    fqdn(peer, dnsDomain),
    +		ExtraDnsLabels:              fqdnList(peer.ExtraDNSLabels, dnsDomain),
     		LoginExpirationEnabled:      peer.LoginExpirationEnabled,
     		LastLogin:                   peer.GetLastLogin(),
     		LoginExpired:                peer.Status.LoginExpired,
    @@ -372,6 +373,7 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
     		UserId:                 peer.UserID,
     		UiVersion:              peer.Meta.UIVersion,
     		DnsLabel:               fqdn(peer, dnsDomain),
    +		ExtraDnsLabels:         fqdnList(peer.ExtraDNSLabels, dnsDomain),
     		LoginExpirationEnabled: peer.LoginExpirationEnabled,
     		LastLogin:              peer.GetLastLogin(),
     		LoginExpired:           peer.Status.LoginExpired,
    @@ -392,3 +394,11 @@ func fqdn(peer *nbpeer.Peer, dnsDomain string) string {
     		return fqdn
     	}
     }
    +func fqdnList(extraLabels []string, dnsDomain string) []string {
    +	fqdnList := make([]string, 0, len(extraLabels))
    +	for _, label := range extraLabels {
    +		fqdn := fmt.Sprintf("%s.%s", label, dnsDomain)
    +		fqdnList = append(fqdnList, fqdn)
    +	}
    +	return fqdnList
    +}
    diff --git a/management/server/http/handlers/routes/routes_handler.go b/management/server/http/handlers/routes/routes_handler.go
    index a29ba4562..6b6c37910 100644
    --- a/management/server/http/handlers/routes/routes_handler.go
    +++ b/management/server/http/handlers/routes/routes_handler.go
    @@ -2,11 +2,8 @@ package routes
     
     import (
     	"encoding/json"
    -	"fmt"
     	"net/http"
     	"net/netip"
    -	"regexp"
    -	"strings"
     	"unicode/utf8"
     
     	"github.com/gorilla/mux"
    @@ -21,7 +18,6 @@ import (
     	"github.com/netbirdio/netbird/route"
     )
     
    -const maxDomains = 32
     const failedToConvertRoute = "failed to convert route to response: %v"
     
     // handler is the routes handler of the account
    @@ -102,7 +98,7 @@ func (h *handler) createRoute(w http.ResponseWriter, r *http.Request) {
     	var networkType route.NetworkType
     	var newPrefix netip.Prefix
     	if req.Domains != nil {
    -		d, err := validateDomains(*req.Domains)
    +		d, err := domain.ValidateDomains(*req.Domains)
     		if err != nil {
     			util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w)
     			return
    @@ -225,7 +221,7 @@ func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) {
     	}
     
     	if req.Domains != nil {
    -		d, err := validateDomains(*req.Domains)
    +		d, err := domain.ValidateDomains(*req.Domains)
     		if err != nil {
     			util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w)
     			return
    @@ -350,34 +346,3 @@ func toRouteResponse(serverRoute *route.Route) (*api.Route, error) {
     	}
     	return route, nil
     }
    -
    -// validateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
    -func validateDomains(domains []string) (domain.List, error) {
    -	if len(domains) == 0 {
    -		return nil, fmt.Errorf("domains list is empty")
    -	}
    -	if len(domains) > maxDomains {
    -		return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
    -	}
    -
    -	domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
    -
    -	var domainList domain.List
    -
    -	for _, d := range domains {
    -		d := strings.ToLower(d)
    -
    -		// handles length and idna conversion
    -		punycode, err := domain.FromString(d)
    -		if err != nil {
    -			return domainList, fmt.Errorf("failed to convert domain to punycode: %s: %v", d, err)
    -		}
    -
    -		if !domainRegex.MatchString(string(punycode)) {
    -			return domainList, fmt.Errorf("invalid domain format: %s", d)
    -		}
    -
    -		domainList = append(domainList, punycode)
    -	}
    -	return domainList, nil
    -}
    diff --git a/management/server/http/handlers/routes/routes_handler_test.go b/management/server/http/handlers/routes/routes_handler_test.go
    index 4064ec361..f3bd79ee4 100644
    --- a/management/server/http/handlers/routes/routes_handler_test.go
    +++ b/management/server/http/handlers/routes/routes_handler_test.go
    @@ -561,96 +561,6 @@ func TestRoutesHandlers(t *testing.T) {
     	}
     }
     
    -func TestValidateDomains(t *testing.T) {
    -	tests := []struct {
    -		name     string
    -		domains  []string
    -		expected domain.List
    -		wantErr  bool
    -	}{
    -		{
    -			name:     "Empty list",
    -			domains:  nil,
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Valid ASCII domain",
    -			domains:  []string{"sub.ex-ample.com"},
    -			expected: domain.List{"sub.ex-ample.com"},
    -			wantErr:  false,
    -		},
    -		{
    -			name:     "Valid Unicode domain",
    -			domains:  []string{"münchen.de"},
    -			expected: domain.List{"xn--mnchen-3ya.de"},
    -			wantErr:  false,
    -		},
    -		{
    -			name:     "Valid Unicode, all labels",
    -			domains:  []string{"中国.中国.中国"},
    -			expected: domain.List{"xn--fiqs8s.xn--fiqs8s.xn--fiqs8s"},
    -			wantErr:  false,
    -		},
    -		{
    -			name:     "With underscores",
    -			domains:  []string{"_jabber._tcp.gmail.com"},
    -			expected: domain.List{"_jabber._tcp.gmail.com"},
    -			wantErr:  false,
    -		},
    -		{
    -			name:     "Invalid domain format",
    -			domains:  []string{"-example.com"},
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Invalid domain format 2",
    -			domains:  []string{"example.com-"},
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Multiple domains valid and invalid",
    -			domains:  []string{"google.com", "invalid,nbdomain.com", "münchen.de"},
    -			expected: domain.List{"google.com"},
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Valid wildcard domain",
    -			domains:  []string{"*.example.com"},
    -			expected: domain.List{"*.example.com"},
    -			wantErr:  false,
    -		},
    -		{
    -			name:     "Wildcard with dot domain",
    -			domains:  []string{".*.example.com"},
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Wildcard with dot domain",
    -			domains:  []string{".*.example.com"},
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Invalid wildcard domain",
    -			domains:  []string{"a.*.example.com"},
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -	}
    -
    -	for _, tt := range tests {
    -		t.Run(tt.name, func(t *testing.T) {
    -			got, err := validateDomains(tt.domains)
    -			assert.Equal(t, tt.wantErr, err != nil)
    -			assert.Equal(t, got, tt.expected)
    -		})
    -	}
    -}
    -
     func toApiRoute(t *testing.T, r *route.Route) *api.Route {
     	t.Helper()
     
    diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler.go b/management/server/http/handlers/setup_keys/setupkeys_handler.go
    index 67e296901..3bd3ef589 100644
    --- a/management/server/http/handlers/setup_keys/setupkeys_handler.go
    +++ b/management/server/http/handlers/setup_keys/setupkeys_handler.go
    @@ -3,6 +3,7 @@ package setup_keys
     import (
     	"context"
     	"encoding/json"
    +
     	"net/http"
     	"time"
     
    @@ -86,8 +87,13 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
     		ephemeral = *req.Ephemeral
     	}
     
    +	var allowExtraDNSLabels bool
    +	if req.AllowExtraDnsLabels != nil {
    +		allowExtraDNSLabels = *req.AllowExtraDnsLabels
    +	}
    +
     	setupKey, err := h.accountManager.CreateSetupKey(r.Context(), accountID, req.Name, types.SetupKeyType(req.Type), expiresIn,
    -		req.AutoGroups, req.UsageLimit, userID, ephemeral)
    +		req.AutoGroups, req.UsageLimit, userID, ephemeral, allowExtraDNSLabels)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
    @@ -237,19 +243,20 @@ func ToResponseBody(key *types.SetupKey) *api.SetupKey {
     	}
     
     	return &api.SetupKey{
    -		Id:         key.Id,
    -		Key:        key.KeySecret,
    -		Name:       key.Name,
    -		Expires:    key.GetExpiresAt(),
    -		Type:       string(key.Type),
    -		Valid:      key.IsValid(),
    -		Revoked:    key.Revoked,
    -		UsedTimes:  key.UsedTimes,
    -		LastUsed:   key.GetLastUsed(),
    -		State:      state,
    -		AutoGroups: key.AutoGroups,
    -		UpdatedAt:  key.UpdatedAt,
    -		UsageLimit: key.UsageLimit,
    -		Ephemeral:  key.Ephemeral,
    +		Id:                  key.Id,
    +		Key:                 key.KeySecret,
    +		Name:                key.Name,
    +		Expires:             key.GetExpiresAt(),
    +		Type:                string(key.Type),
    +		Valid:               key.IsValid(),
    +		Revoked:             key.Revoked,
    +		UsedTimes:           key.UsedTimes,
    +		LastUsed:            key.GetLastUsed(),
    +		State:               state,
    +		AutoGroups:          key.AutoGroups,
    +		UpdatedAt:           key.UpdatedAt,
    +		UsageLimit:          key.UsageLimit,
    +		Ephemeral:           key.Ephemeral,
    +		AllowExtraDnsLabels: key.AllowExtraDNSLabels,
     	}
     }
    diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    index f56227c10..4912f9639 100644
    --- a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    +++ b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    @@ -37,11 +37,12 @@ func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKe
     				return claims.AccountId, claims.UserId, nil
     			},
     			CreateSetupKeyFunc: func(_ context.Context, _ string, keyName string, typ types.SetupKeyType, _ time.Duration, _ []string,
    -				_ int, _ string, ephemeral bool,
    +				_ int, _ string, ephemeral bool, allowExtraDNSLabels bool,
     			) (*types.SetupKey, error) {
     				if keyName == newKey.Name || typ != newKey.Type {
     					nk := newKey.Copy()
     					nk.Ephemeral = ephemeral
    +					nk.AllowExtraDNSLabels = allowExtraDNSLabels
     					return nk, nil
     				}
     				return nil, fmt.Errorf("failed creating setup key")
    @@ -94,7 +95,7 @@ func TestSetupKeysHandlers(t *testing.T) {
     	adminUser := types.NewAdminUser("test_user")
     
     	newSetupKey, plainKey := types.GenerateSetupKey(newSetupKeyName, types.SetupKeyReusable, 0, []string{"group-1"},
    -		types.SetupKeyUnlimitedUsage, true)
    +		types.SetupKeyUnlimitedUsage, true, false)
     	newSetupKey.Key = plainKey
     	updatedDefaultSetupKey := defaultSetupKey.Copy()
     	updatedDefaultSetupKey.AutoGroups = []string{"group-1"}
    diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go
    index bcdf75b8c..9c2ce5ad2 100644
    --- a/management/server/management_proto_test.go
    +++ b/management/server/management_proto_test.go
    @@ -714,7 +714,7 @@ func Test_LoginPerformance(t *testing.T) {
     						return
     					}
     
    -					setupKey, err := am.CreateSetupKey(context.Background(), account.Id, fmt.Sprintf("key-%d", j), types.SetupKeyReusable, time.Hour, nil, 0, fmt.Sprintf("user-%d", j), false)
    +					setupKey, err := am.CreateSetupKey(context.Background(), account.Id, fmt.Sprintf("key-%d", j), types.SetupKeyReusable, time.Hour, nil, 0, fmt.Sprintf("user-%d", j), false, false)
     					if err != nil {
     						t.Logf("error creating setup key: %v", err)
     						return
    diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go
    index b20eb87bb..b2a90f156 100644
    --- a/management/server/mock_server/account_mock.go
    +++ b/management/server/mock_server/account_mock.go
    @@ -25,7 +25,7 @@ type MockAccountManager struct {
     	GetOrCreateAccountByUserFunc func(ctx context.Context, userId, domain string) (*types.Account, error)
     	GetAccountFunc               func(ctx context.Context, accountID string) (*types.Account, error)
     	CreateSetupKeyFunc           func(ctx context.Context, accountId string, keyName string, keyType types.SetupKeyType,
    -		expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error)
    +		expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error)
     	GetSetupKeyFunc                     func(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error)
     	AccountExistsFunc                   func(ctx context.Context, accountID string) (bool, error)
     	GetAccountIDByUserIdFunc            func(ctx context.Context, userId, domain string) (string, error)
    @@ -205,9 +205,10 @@ func (am *MockAccountManager) CreateSetupKey(
     	usageLimit int,
     	userID string,
     	ephemeral bool,
    +	allowExtraDNSLabels bool,
     ) (*types.SetupKey, error) {
     	if am.CreateSetupKeyFunc != nil {
    -		return am.CreateSetupKeyFunc(ctx, accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID, ephemeral)
    +		return am.CreateSetupKeyFunc(ctx, accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID, ephemeral, allowExtraDNSLabels)
     	}
     	return nil, status.Errorf(codes.Unimplemented, "method CreateSetupKey is not implemented")
     }
    diff --git a/management/server/peer.go b/management/server/peer.go
    index efd9c64e3..c9b0fcfee 100644
    --- a/management/server/peer.go
    +++ b/management/server/peer.go
    @@ -15,6 +15,7 @@ import (
     	log "github.com/sirupsen/logrus"
     	"golang.org/x/exp/maps"
     
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     
     	"github.com/netbirdio/netbird/management/server/idp"
    @@ -53,6 +54,9 @@ type PeerLogin struct {
     	SetupKey string
     	// ConnectionIP is the real IP of the peer
     	ConnectionIP net.IP
    +
    +	// ExtraDNSLabels is a list of extra DNS labels that the peer wants to use
    +	ExtraDNSLabels []string
     }
     
     // GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if
    @@ -502,6 +506,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
     		var setupKeyName string
     		var ephemeral bool
     		var groupsToAdd []string
    +		var allowExtraDNSLabels bool
     		if addedByUser {
     			user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthUpdate, userID)
     			if err != nil {
    @@ -527,6 +532,11 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
     			ephemeral = sk.Ephemeral
     			setupKeyID = sk.Id
     			setupKeyName = sk.Name
    +			allowExtraDNSLabels = sk.AllowExtraDNSLabels
    +
    +			if !sk.AllowExtraDNSLabels && len(peer.ExtraDNSLabels) > 0 {
    +				return status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key doesn't allow extra DNS labels")
    +			}
     		}
     
     		if (strings.ToLower(peer.Meta.Hostname) == "iphone" || strings.ToLower(peer.Meta.Hostname) == "ipad") && userID != "" {
    @@ -567,6 +577,8 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
     			Ephemeral:                   ephemeral,
     			Location:                    peer.Location,
     			InactivityExpirationEnabled: addedByUser,
    +			ExtraDNSLabels:              peer.ExtraDNSLabels,
    +			AllowExtraDNSLabels:         allowExtraDNSLabels,
     		}
     		opEvent.TargetID = newPeer.ID
     		opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain())
    @@ -860,6 +872,20 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login PeerLogin)
     			shouldStorePeer = true
     		}
     
    +		if !peer.AllowExtraDNSLabels && len(login.ExtraDNSLabels) > 0 {
    +			return status.Errorf(status.PreconditionFailed, "couldn't login peer: setup key doesn't allow extra DNS labels")
    +		}
    +
    +		extraLabels, err := domain.ValidateDomainsStrSlice(login.ExtraDNSLabels)
    +		if err != nil {
    +			return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err)
    +		}
    +
    +		if !slices.Equal(peer.ExtraDNSLabels, extraLabels) {
    +			peer.ExtraDNSLabels = extraLabels
    +			shouldStorePeer = true
    +		}
    +
     		if shouldStorePeer {
     			if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil {
     				return err
    diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go
    index 199c7c89d..afda55d17 100644
    --- a/management/server/peer/peer.go
    +++ b/management/server/peer/peer.go
    @@ -49,6 +49,11 @@ type Peer struct {
     	Ephemeral bool `gorm:"index"`
     	// Geo location based on connection IP
     	Location Location `gorm:"embedded;embeddedPrefix:location_"`
    +
    +	// ExtraDNSLabels is a list of additional DNS labels that can be used to resolve the peer
    +	ExtraDNSLabels []string `gorm:"serializer:json"`
    +	// AllowExtraDNSLabels indicates whether the peer allows extra DNS labels to be used for resolving the peer
    +	AllowExtraDNSLabels bool
     }
     
     type PeerStatus struct { //nolint:revive
    @@ -202,6 +207,8 @@ func (p *Peer) Copy() *Peer {
     		Ephemeral:                   p.Ephemeral,
     		Location:                    p.Location,
     		InactivityExpirationEnabled: p.InactivityExpirationEnabled,
    +		ExtraDNSLabels:              slices.Clone(p.ExtraDNSLabels),
    +		AllowExtraDNSLabels:         p.AllowExtraDNSLabels,
     	}
     }
     
    diff --git a/management/server/peer_test.go b/management/server/peer_test.go
    index 6894d092d..9deb8e456 100644
    --- a/management/server/peer_test.go
    +++ b/management/server/peer_test.go
    @@ -168,7 +168,7 @@ func TestAccountManager_GetNetworkMap(t *testing.T) {
     		t.Fatal(err)
     	}
     
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     		return
    @@ -417,7 +417,7 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) {
     		t.Fatal(err)
     	}
     
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     		return
    @@ -489,7 +489,7 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
     	}
     
     	// two peers one added by a regular user and one with a setup key
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, adminUser, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, adminUser, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     		return
    diff --git a/management/server/setupkey.go b/management/server/setupkey.go
    index f2f1aad45..b0bdad4e5 100644
    --- a/management/server/setupkey.go
    +++ b/management/server/setupkey.go
    @@ -52,7 +52,7 @@ type SetupKeyUpdateOperation struct {
     // CreateSetupKey generates a new setup key with a given name, type, list of groups IDs to auto-assign to peers registered with this key,
     // and adds it to the specified account. A list of autoGroups IDs can be empty.
     func (am *DefaultAccountManager) CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType,
    -	expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error) {
    +	expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error) {
     	unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
     	defer unlock()
     
    @@ -78,7 +78,7 @@ func (am *DefaultAccountManager) CreateSetupKey(ctx context.Context, accountID s
     			return status.Errorf(status.InvalidArgument, "invalid auto groups: %v", err)
     		}
     
    -		setupKey, plainKey = types.GenerateSetupKey(keyName, keyType, expiresIn, autoGroups, usageLimit, ephemeral)
    +		setupKey, plainKey = types.GenerateSetupKey(keyName, keyType, expiresIn, autoGroups, usageLimit, ephemeral, allowExtraDNSLabels)
     		setupKey.AccountID = accountID
     
     		events := am.prepareSetupKeyEvents(ctx, transaction, accountID, userID, autoGroups, nil, setupKey)
    diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go
    index e225ec54b..6e1e1cf7d 100644
    --- a/management/server/setupkey_test.go
    +++ b/management/server/setupkey_test.go
    @@ -50,7 +50,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) {
     	keyName := "my-test-key"
     
     	key, err := manager.CreateSetupKey(context.Background(), account.Id, keyName, types.SetupKeyReusable, expiresIn, []string{},
    -		types.SetupKeyUnlimitedUsage, userID, false)
    +		types.SetupKeyUnlimitedUsage, userID, false, false)
     	if err != nil {
     		t.Fatal(err)
     	}
    @@ -168,7 +168,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
     	for _, tCase := range []testCase{testCase1, testCase2, testCase3} {
     		t.Run(tCase.name, func(t *testing.T) {
     			key, err := manager.CreateSetupKey(context.Background(), account.Id, tCase.expectedKeyName, types.SetupKeyReusable, expiresIn,
    -				tCase.expectedGroups, types.SetupKeyUnlimitedUsage, userID, false)
    +				tCase.expectedGroups, types.SetupKeyUnlimitedUsage, userID, false, false)
     
     			if tCase.expectedFailure {
     				if err == nil {
    @@ -210,7 +210,7 @@ func TestGetSetupKeys(t *testing.T) {
     		t.Fatal(err)
     	}
     
    -	plainKey, err := manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false)
    +	plainKey, err := manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false, false)
     	if err != nil {
     		t.Fatal(err)
     	}
    @@ -275,7 +275,7 @@ func TestGenerateSetupKey(t *testing.T) {
     	expectedUpdatedAt := time.Now().UTC()
     	var expectedAutoGroups []string
     
    -	key, plain := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	key, plain := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     
     	assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt,
     		expectedExpiresAt, strconv.Itoa(int(types.Hash(plain))), expectedUpdatedAt, expectedAutoGroups, true)
    @@ -283,33 +283,33 @@ func TestGenerateSetupKey(t *testing.T) {
     }
     
     func TestSetupKey_IsValid(t *testing.T) {
    -	validKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	validKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	if !validKey.IsValid() {
     		t.Errorf("expected key to be valid, got invalid %v", validKey)
     	}
     
     	// expired
    -	expiredKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, -time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	expiredKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, -time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	if expiredKey.IsValid() {
     		t.Errorf("expected key to be invalid due to expiration, got valid %v", expiredKey)
     	}
     
     	// revoked
    -	revokedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	revokedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	revokedKey.Revoked = true
     	if revokedKey.IsValid() {
     		t.Errorf("expected revoked key to be invalid, got valid %v", revokedKey)
     	}
     
     	// overused
    -	overUsedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	overUsedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	overUsedKey.UsedTimes = 1
     	if overUsedKey.IsValid() {
     		t.Errorf("expected overused key to be invalid, got valid %v", overUsedKey)
     	}
     
     	// overused
    -	reusableKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyReusable, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	reusableKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyReusable, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	reusableKey.UsedTimes = 99
     	if !reusableKey.IsValid() {
     		t.Errorf("expected reusable key to be valid when used many times, got valid %v", reusableKey)
    @@ -388,7 +388,7 @@ func isValidBase64SHA256(encodedKey string) bool {
     
     func TestSetupKey_Copy(t *testing.T) {
     
    -	key, _ := types.GenerateSetupKey("key name", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	key, _ := types.GenerateSetupKey("key name", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	keyCopy := key.Copy()
     
     	assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.GetExpiresAt(), key.Id,
    @@ -436,7 +436,7 @@ func TestSetupKeyAccountPeersUpdate(t *testing.T) {
     			close(done)
     		}()
     
    -		setupKey, err = manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
    +		setupKey, err = manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
     		assert.NoError(t, err)
     
     		select {
    @@ -477,7 +477,7 @@ func TestDefaultAccountManager_CreateSetupKey_ShouldNotAllowToUpdateRevokedKey(t
     		t.Fatal(err)
     	}
     
    -	key, err := manager.CreateSetupKey(context.Background(), account.Id, "testName", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false)
    +	key, err := manager.CreateSetupKey(context.Background(), account.Id, "testName", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false, false)
     	assert.NoError(t, err)
     
     	// revoke the key
    diff --git a/management/server/types/account.go b/management/server/types/account.go
    index 0df15816f..4c68b9523 100644
    --- a/management/server/types/account.go
    +++ b/management/server/types/account.go
    @@ -459,8 +459,23 @@ func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdn
     			TTL:   defaultTTL,
     			RData: peer.IP.String(),
     		})
    -
     		sb.Reset()
    +
    +		for _, extraLabel := range peer.ExtraDNSLabels {
    +			sb.Grow(len(extraLabel) + len(domainSuffix))
    +			sb.WriteString(extraLabel)
    +			sb.WriteString(domainSuffix)
    +
    +			customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
    +				Name:  sb.String(),
    +				Type:  int(dns.TypeA),
    +				Class: nbdns.DefaultClass,
    +				TTL:   defaultTTL,
    +				RData: peer.IP.String(),
    +			})
    +			sb.Reset()
    +		}
    +
     	}
     
     	go func() {
    diff --git a/management/server/types/setupkey.go b/management/server/types/setupkey.go
    index 2cd835289..ab8e46bea 100644
    --- a/management/server/types/setupkey.go
    +++ b/management/server/types/setupkey.go
    @@ -10,6 +10,7 @@ import (
     	"unicode/utf8"
     
     	"github.com/google/uuid"
    +
     	"github.com/netbirdio/netbird/management/server/util"
     )
     
    @@ -54,6 +55,8 @@ type SetupKey struct {
     	UsageLimit int
     	// Ephemeral indicate if the peers will be ephemeral or not
     	Ephemeral bool
    +	// AllowExtraDNSLabels indicates if the key allows extra DNS labels
    +	AllowExtraDNSLabels bool
     }
     
     // Copy copies SetupKey to a new object
    @@ -64,21 +67,22 @@ func (key *SetupKey) Copy() *SetupKey {
     		key.UpdatedAt = key.CreatedAt
     	}
     	return &SetupKey{
    -		Id:         key.Id,
    -		AccountID:  key.AccountID,
    -		Key:        key.Key,
    -		KeySecret:  key.KeySecret,
    -		Name:       key.Name,
    -		Type:       key.Type,
    -		CreatedAt:  key.CreatedAt,
    -		ExpiresAt:  key.ExpiresAt,
    -		UpdatedAt:  key.UpdatedAt,
    -		Revoked:    key.Revoked,
    -		UsedTimes:  key.UsedTimes,
    -		LastUsed:   key.LastUsed,
    -		AutoGroups: autoGroups,
    -		UsageLimit: key.UsageLimit,
    -		Ephemeral:  key.Ephemeral,
    +		Id:                  key.Id,
    +		AccountID:           key.AccountID,
    +		Key:                 key.Key,
    +		KeySecret:           key.KeySecret,
    +		Name:                key.Name,
    +		Type:                key.Type,
    +		CreatedAt:           key.CreatedAt,
    +		ExpiresAt:           key.ExpiresAt,
    +		UpdatedAt:           key.UpdatedAt,
    +		Revoked:             key.Revoked,
    +		UsedTimes:           key.UsedTimes,
    +		LastUsed:            key.LastUsed,
    +		AutoGroups:          autoGroups,
    +		UsageLimit:          key.UsageLimit,
    +		Ephemeral:           key.Ephemeral,
    +		AllowExtraDNSLabels: key.AllowExtraDNSLabels,
     	}
     }
     
    @@ -150,7 +154,7 @@ func (key *SetupKey) IsOverUsed() bool {
     
     // GenerateSetupKey generates a new setup key
     func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoGroups []string,
    -	usageLimit int, ephemeral bool) (*SetupKey, string) {
    +	usageLimit int, ephemeral bool, allowExtraDNSLabels bool) (*SetupKey, string) {
     	key := strings.ToUpper(uuid.New().String())
     	limit := usageLimit
     	if t == SetupKeyOneOff {
    @@ -166,26 +170,27 @@ func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoG
     	encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
     
     	return &SetupKey{
    -		Id:         strconv.Itoa(int(Hash(key))),
    -		Key:        encodedHashedKey,
    -		KeySecret:  HiddenKey(key, 4),
    -		Name:       name,
    -		Type:       t,
    -		CreatedAt:  time.Now().UTC(),
    -		ExpiresAt:  expiresAt,
    -		UpdatedAt:  time.Now().UTC(),
    -		Revoked:    false,
    -		UsedTimes:  0,
    -		AutoGroups: autoGroups,
    -		UsageLimit: limit,
    -		Ephemeral:  ephemeral,
    +		Id:                  strconv.Itoa(int(Hash(key))),
    +		Key:                 encodedHashedKey,
    +		KeySecret:           HiddenKey(key, 4),
    +		Name:                name,
    +		Type:                t,
    +		CreatedAt:           time.Now().UTC(),
    +		ExpiresAt:           expiresAt,
    +		UpdatedAt:           time.Now().UTC(),
    +		Revoked:             false,
    +		UsedTimes:           0,
    +		AutoGroups:          autoGroups,
    +		UsageLimit:          limit,
    +		Ephemeral:           ephemeral,
    +		AllowExtraDNSLabels: allowExtraDNSLabels,
     	}, key
     }
     
     // GenerateDefaultSetupKey generates a default reusable setup key with an unlimited usage and 30 days expiration
     func GenerateDefaultSetupKey() (*SetupKey, string) {
     	return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{},
    -		SetupKeyUnlimitedUsage, false)
    +		SetupKeyUnlimitedUsage, false, false)
     }
     
     func Hash(s string) uint32 {
    
    From 631ef4ed28a5b1fcd4dfe53d645c169deb2882b0 Mon Sep 17 00:00:00 2001
    From: Viktor Liu <17948409+lixmal@users.noreply.github.com>
    Date: Thu, 20 Feb 2025 13:22:03 +0100
    Subject: [PATCH 15/23] [client] Add embeddable library (#3239)
    
    ---
     client/embed/doc.go                        | 167 ++++++++++++
     client/embed/embed.go                      | 296 +++++++++++++++++++++
     client/firewall/uspfilter/uspfilter.go     |  27 +-
     client/iface/device.go                     |   3 +
     client/iface/device/device_android.go      |   5 +
     client/iface/device/device_darwin.go       |   5 +
     client/iface/device/device_ios.go          |   5 +
     client/iface/device/device_kernel_unix.go  |   5 +
     client/iface/device/device_netstack.go     |  24 +-
     client/iface/device/device_usp_unix.go     |   5 +
     client/iface/device/device_windows.go      |   5 +
     client/iface/device_android.go             |   3 +
     client/iface/iface.go                      |   9 +
     client/iface/iface_moc.go                  |   6 +
     client/iface/iwginterface.go               |   2 +
     client/iface/iwginterface_windows.go       |   2 +
     client/iface/netstack/env.go               |   4 +-
     client/iface/netstack/tun.go               |  42 ++-
     client/internal/dns/service_memory.go      |  24 +-
     client/internal/dns/service_memory_test.go |   4 +-
     client/internal/engine.go                  |  36 ++-
     util/net/net.go                            |  20 ++
     22 files changed, 648 insertions(+), 51 deletions(-)
     create mode 100644 client/embed/doc.go
     create mode 100644 client/embed/embed.go
    
    diff --git a/client/embed/doc.go b/client/embed/doc.go
    new file mode 100644
    index 000000000..069d53ebf
    --- /dev/null
    +++ b/client/embed/doc.go
    @@ -0,0 +1,167 @@
    +// Package embed provides a way to embed the NetBird client directly
    +// into Go programs without requiring a separate NetBird client installation.
    +package embed
    +
    +// Basic Usage:
    +//
    +//	client, err := embed.New(embed.Options{
    +//	    DeviceName:    "my-service",
    +//	    SetupKey:      os.Getenv("NB_SETUP_KEY"),
    +//	    ManagementURL: os.Getenv("NB_MANAGEMENT_URL"),
    +//	})
    +//	if err != nil {
    +//	    log.Fatal(err)
    +//	}
    +//
    +//	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    +//	defer cancel()
    +//	if err := client.Start(ctx); err != nil {
    +//	    log.Fatal(err)
    +//	}
    +//
    +// Complete HTTP Server Example:
    +//
    +//	package main
    +//
    +//	import (
    +//	    "context"
    +//	    "fmt"
    +//	    "log"
    +//	    "net/http"
    +//	    "os"
    +//	    "os/signal"
    +//	    "syscall"
    +//	    "time"
    +//
    +//	    netbird "github.com/netbirdio/netbird/client/embed"
    +//	)
    +//
    +//	func main() {
    +//	    // Create client with setup key and device name
    +//	    client, err := netbird.New(netbird.Options{
    +//	        DeviceName:    "http-server",
    +//	        SetupKey:      os.Getenv("NB_SETUP_KEY"),
    +//	        ManagementURL: os.Getenv("NB_MANAGEMENT_URL"),
    +//	        LogOutput:     io.Discard,
    +//	    })
    +//	    if err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    // Start with timeout
    +//	    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    +//	    defer cancel()
    +//	    if err := client.Start(ctx); err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    // Create HTTP server
    +//	    mux := http.NewServeMux()
    +//	    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    +//	        fmt.Printf("Request from %s: %s %s\n", r.RemoteAddr, r.Method, r.URL.Path)
    +//	        fmt.Fprintf(w, "Hello from netbird!")
    +//	    })
    +//
    +//	    // Listen on netbird network
    +//	    l, err := client.ListenTCP(":8080")
    +//	    if err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    server := &http.Server{Handler: mux}
    +//	    go func() {
    +//	        if err := server.Serve(l); !errors.Is(err, http.ErrServerClosed) {
    +//	            log.Printf("HTTP server error: %v", err)
    +//	        }
    +//	    }()
    +//
    +//	    log.Printf("HTTP server listening on netbird network port 8080")
    +//
    +//	    // Handle shutdown
    +//	    stop := make(chan os.Signal, 1)
    +//	    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
    +//	    <-stop
    +//
    +//	    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    +//	    defer cancel()
    +//
    +//	    if err := server.Shutdown(shutdownCtx); err != nil {
    +//	        log.Printf("HTTP shutdown error: %v", err)
    +//	    }
    +//	    if err := client.Stop(shutdownCtx); err != nil {
    +//	        log.Printf("Netbird shutdown error: %v", err)
    +//	    }
    +//	}
    +//
    +// Complete HTTP Client Example:
    +//
    +//	package main
    +//
    +//	import (
    +//	    "context"
    +//	    "fmt"
    +//	    "io"
    +//	    "log"
    +//	    "os"
    +//	    "time"
    +//
    +//	    netbird "github.com/netbirdio/netbird/client/embed"
    +//	)
    +//
    +//	func main() {
    +//	    // Create client with setup key and device name
    +//	    client, err := netbird.New(netbird.Options{
    +//	        DeviceName:    "http-client",
    +//	        SetupKey:      os.Getenv("NB_SETUP_KEY"),
    +//	        ManagementURL: os.Getenv("NB_MANAGEMENT_URL"),
    +//	        LogOutput:     io.Discard,
    +//	    })
    +//	    if err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    // Start with timeout
    +//	    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    +//	    defer cancel()
    +//
    +//	    if err := client.Start(ctx); err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    // Create HTTP client that uses netbird network
    +//	    httpClient := client.NewHTTPClient()
    +//	    httpClient.Timeout = 10 * time.Second
    +//
    +//	    // Make request to server in netbird network
    +//	    target := os.Getenv("NB_TARGET")
    +//	    resp, err := httpClient.Get(target)
    +//	    if err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//	    defer resp.Body.Close()
    +//
    +//	    // Read and print response
    +//	    body, err := io.ReadAll(resp.Body)
    +//	    if err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    fmt.Printf("Response from server: %s\n", string(body))
    +//
    +//	    // Clean shutdown
    +//	    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    +//	    defer cancel()
    +//
    +//	    if err := client.Stop(shutdownCtx); err != nil {
    +//	        log.Printf("Netbird shutdown error: %v", err)
    +//	    }
    +//	}
    +//
    +// The package provides several methods for network operations:
    +//   - Dial: Creates outbound connections
    +//   - ListenTCP: Creates TCP listeners
    +//   - ListenUDP: Creates UDP listeners
    +//
    +// By default, the embed package uses userspace networking mode, which doesn't
    +// require root/admin privileges. For production deployments, consider setting
    +// appropriate config and state paths for persistence.
    diff --git a/client/embed/embed.go b/client/embed/embed.go
    new file mode 100644
    index 000000000..9ded618c5
    --- /dev/null
    +++ b/client/embed/embed.go
    @@ -0,0 +1,296 @@
    +package embed
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"io"
    +	"net"
    +	"net/http"
    +	"net/netip"
    +	"os"
    +	"sync"
    +
    +	"github.com/sirupsen/logrus"
    +	wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
    +
    +	"github.com/netbirdio/netbird/client/iface/netstack"
    +	"github.com/netbirdio/netbird/client/internal"
    +	"github.com/netbirdio/netbird/client/internal/peer"
    +	"github.com/netbirdio/netbird/client/system"
    +)
    +
    +var ErrClientAlreadyStarted = errors.New("client already started")
    +var ErrClientNotStarted = errors.New("client not started")
    +
    +// Client manages a netbird embedded client instance
    +type Client struct {
    +	deviceName string
    +	config     *internal.Config
    +	mu         sync.Mutex
    +	cancel     context.CancelFunc
    +	setupKey   string
    +	connect    *internal.ConnectClient
    +}
    +
    +// Options configures a new Client
    +type Options struct {
    +	// DeviceName is this peer's name in the network
    +	DeviceName string
    +	// SetupKey is used for authentication
    +	SetupKey string
    +	// ManagementURL overrides the default management server URL
    +	ManagementURL string
    +	// PreSharedKey is the pre-shared key for the WireGuard interface
    +	PreSharedKey string
    +	// LogOutput is the output destination for logs (defaults to os.Stderr if nil)
    +	LogOutput io.Writer
    +	// LogLevel sets the logging level (defaults to info if empty)
    +	LogLevel string
    +	// NoUserspace disables the userspace networking mode. Needs admin/root privileges
    +	NoUserspace bool
    +	// ConfigPath is the path to the netbird config file. If empty, the config will be stored in memory and not persisted.
    +	ConfigPath string
    +	// StatePath is the path to the netbird state file
    +	StatePath string
    +	// DisableClientRoutes disables the client routes
    +	DisableClientRoutes bool
    +}
    +
    +// New creates a new netbird embedded client
    +func New(opts Options) (*Client, error) {
    +	if opts.LogOutput != nil {
    +		logrus.SetOutput(opts.LogOutput)
    +	}
    +
    +	if opts.LogLevel != "" {
    +		level, err := logrus.ParseLevel(opts.LogLevel)
    +		if err != nil {
    +			return nil, fmt.Errorf("parse log level: %w", err)
    +		}
    +		logrus.SetLevel(level)
    +	}
    +
    +	if !opts.NoUserspace {
    +		if err := os.Setenv(netstack.EnvUseNetstackMode, "true"); err != nil {
    +			return nil, fmt.Errorf("setenv: %w", err)
    +		}
    +		if err := os.Setenv(netstack.EnvSkipProxy, "true"); err != nil {
    +			return nil, fmt.Errorf("setenv: %w", err)
    +		}
    +	}
    +
    +	if opts.StatePath != "" {
    +		// TODO: Disable state if path not provided
    +		if err := os.Setenv("NB_DNS_STATE_FILE", opts.StatePath); err != nil {
    +			return nil, fmt.Errorf("setenv: %w", err)
    +		}
    +	}
    +
    +	t := true
    +	var config *internal.Config
    +	var err error
    +	input := internal.ConfigInput{
    +		ConfigPath:          opts.ConfigPath,
    +		ManagementURL:       opts.ManagementURL,
    +		PreSharedKey:        &opts.PreSharedKey,
    +		DisableServerRoutes: &t,
    +		DisableClientRoutes: &opts.DisableClientRoutes,
    +	}
    +	if opts.ConfigPath != "" {
    +		config, err = internal.UpdateOrCreateConfig(input)
    +	} else {
    +		config, err = internal.CreateInMemoryConfig(input)
    +	}
    +	if err != nil {
    +		return nil, fmt.Errorf("create config: %w", err)
    +	}
    +
    +	return &Client{
    +		deviceName: opts.DeviceName,
    +		setupKey:   opts.SetupKey,
    +		config:     config,
    +	}, nil
    +}
    +
    +// Start begins client operation and blocks until the engine has been started successfully or a startup error occurs.
    +// Pass a context with a deadline to limit the time spent waiting for the engine to start.
    +func (c *Client) Start(startCtx context.Context) error {
    +	c.mu.Lock()
    +	defer c.mu.Unlock()
    +	if c.cancel != nil {
    +		return ErrClientAlreadyStarted
    +	}
    +
    +	ctx := internal.CtxInitState(context.Background())
    +	// nolint:staticcheck
    +	ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName)
    +	if err := internal.Login(ctx, c.config, c.setupKey, ""); err != nil {
    +		return fmt.Errorf("login: %w", err)
    +	}
    +
    +	recorder := peer.NewRecorder(c.config.ManagementURL.String())
    +	client := internal.NewConnectClient(ctx, c.config, recorder)
    +
    +	// either startup error (permanent backoff err) or nil err (successful engine up)
    +	// TODO: make after-startup backoff err available
    +	run := make(chan error, 1)
    +	go func() {
    +		if err := client.Run(run); err != nil {
    +			run <- err
    +		}
    +	}()
    +
    +	select {
    +	case <-startCtx.Done():
    +		if stopErr := client.Stop(); stopErr != nil {
    +			return fmt.Errorf("stop error after context done. Stop error: %w. Context done: %w", stopErr, startCtx.Err())
    +		}
    +		return startCtx.Err()
    +	case err := <-run:
    +		if err != nil {
    +			if stopErr := client.Stop(); stopErr != nil {
    +				return fmt.Errorf("stop error after failed to startup. Stop error: %w. Start error: %w", stopErr, err)
    +			}
    +			return fmt.Errorf("startup: %w", err)
    +		}
    +	}
    +
    +	c.connect = client
    +
    +	return nil
    +}
    +
    +// Stop gracefully stops the client.
    +// Pass a context with a deadline to limit the time spent waiting for the engine to stop.
    +func (c *Client) Stop(ctx context.Context) error {
    +	c.mu.Lock()
    +	defer c.mu.Unlock()
    +
    +	if c.connect == nil {
    +		return ErrClientNotStarted
    +	}
    +
    +	done := make(chan error, 1)
    +	go func() {
    +		done <- c.connect.Stop()
    +	}()
    +
    +	select {
    +	case <-ctx.Done():
    +		c.cancel = nil
    +		return ctx.Err()
    +	case err := <-done:
    +		c.cancel = nil
    +		if err != nil {
    +			return fmt.Errorf("stop: %w", err)
    +		}
    +		return nil
    +	}
    +}
    +
    +// Dial dials a network address in the netbird network.
    +// Not applicable if the userspace networking mode is disabled.
    +func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, error) {
    +	c.mu.Lock()
    +	connect := c.connect
    +	if connect == nil {
    +		c.mu.Unlock()
    +		return nil, ErrClientNotStarted
    +	}
    +	c.mu.Unlock()
    +
    +	engine := connect.Engine()
    +	if engine == nil {
    +		return nil, errors.New("engine not started")
    +	}
    +
    +	nsnet, err := engine.GetNet()
    +	if err != nil {
    +		return nil, fmt.Errorf("get net: %w", err)
    +	}
    +
    +	return nsnet.DialContext(ctx, network, address)
    +}
    +
    +// ListenTCP listens on the given address in the netbird network
    +// Not applicable if the userspace networking mode is disabled.
    +func (c *Client) ListenTCP(address string) (net.Listener, error) {
    +	nsnet, addr, err := c.getNet()
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	_, port, err := net.SplitHostPort(address)
    +	if err != nil {
    +		return nil, fmt.Errorf("split host port: %w", err)
    +	}
    +	listenAddr := fmt.Sprintf("%s:%s", addr, port)
    +
    +	tcpAddr, err := net.ResolveTCPAddr("tcp", listenAddr)
    +	if err != nil {
    +		return nil, fmt.Errorf("resolve: %w", err)
    +	}
    +	return nsnet.ListenTCP(tcpAddr)
    +}
    +
    +// ListenUDP listens on the given address in the netbird network
    +// Not applicable if the userspace networking mode is disabled.
    +func (c *Client) ListenUDP(address string) (net.PacketConn, error) {
    +	nsnet, addr, err := c.getNet()
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	_, port, err := net.SplitHostPort(address)
    +	if err != nil {
    +		return nil, fmt.Errorf("split host port: %w", err)
    +	}
    +	listenAddr := fmt.Sprintf("%s:%s", addr, port)
    +
    +	udpAddr, err := net.ResolveUDPAddr("udp", listenAddr)
    +	if err != nil {
    +		return nil, fmt.Errorf("resolve: %w", err)
    +	}
    +
    +	return nsnet.ListenUDP(udpAddr)
    +}
    +
    +// NewHTTPClient returns a configured http.Client that uses the netbird network for requests.
    +// Not applicable if the userspace networking mode is disabled.
    +func (c *Client) NewHTTPClient() *http.Client {
    +	transport := &http.Transport{
    +		DialContext: c.Dial,
    +	}
    +
    +	return &http.Client{
    +		Transport: transport,
    +	}
    +}
    +
    +func (c *Client) getNet() (*wgnetstack.Net, netip.Addr, error) {
    +	c.mu.Lock()
    +	connect := c.connect
    +	if connect == nil {
    +		c.mu.Unlock()
    +		return nil, netip.Addr{}, errors.New("client not started")
    +	}
    +	c.mu.Unlock()
    +
    +	engine := connect.Engine()
    +	if engine == nil {
    +		return nil, netip.Addr{}, errors.New("engine not started")
    +	}
    +
    +	addr, err := engine.Address()
    +	if err != nil {
    +		return nil, netip.Addr{}, fmt.Errorf("engine address: %w", err)
    +	}
    +
    +	nsnet, err := engine.GetNet()
    +	if err != nil {
    +		return nil, netip.Addr{}, fmt.Errorf("get net: %w", err)
    +	}
    +
    +	return nsnet, addr, nil
    +}
    diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go
    index 5bb225ccd..50f48a5c4 100644
    --- a/client/firewall/uspfilter/uspfilter.go
    +++ b/client/firewall/uspfilter/uspfilter.go
    @@ -173,8 +173,7 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe
     		stateful:            !disableConntrack,
     		logger:              nblog.NewFromLogrus(log.StandardLogger()),
     		netstack:            netstack.IsEnabled(),
    -		// default true for non-netstack, for netstack only if explicitly enabled
    -		localForwarding: !netstack.IsEnabled() || enableLocalForwarding,
    +		localForwarding:     enableLocalForwarding,
     	}
     
     	if err := m.localipmanager.UpdateLocalIPs(iface); err != nil {
    @@ -647,11 +646,6 @@ func (m *Manager) dropFilter(packetData []byte) bool {
     // handleLocalTraffic handles local traffic.
     // If it returns true, the packet should be dropped.
     func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool {
    -	if !m.localForwarding {
    -		m.logger.Trace("Dropping local packet (local forwarding disabled): src=%s dst=%s", srcIP, dstIP)
    -		return true
    -	}
    -
     	if m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) {
     		m.logger.Trace("Dropping local packet (ACL denied): src=%s dst=%s",
     			srcIP, dstIP)
    @@ -660,22 +654,29 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP net.IP, packetData
     
     	// if running in netstack mode we need to pass this to the forwarder
     	if m.netstack {
    -		m.handleNetstackLocalTraffic(packetData)
    -
    -		// don't process this packet further
    -		return true
    +		return m.handleNetstackLocalTraffic(packetData)
     	}
     
     	return false
     }
    -func (m *Manager) handleNetstackLocalTraffic(packetData []byte) {
    +
    +func (m *Manager) handleNetstackLocalTraffic(packetData []byte) bool {
    +	if !m.localForwarding {
    +		// pass to virtual tcp/ip stack to be picked up by listeners
    +		return false
    +	}
    +
     	if m.forwarder == nil {
    -		return
    +		m.logger.Trace("Dropping local packet (forwarder not initialized)")
    +		return true
     	}
     
     	if err := m.forwarder.InjectIncomingPacket(packetData); err != nil {
     		m.logger.Error("Failed to inject local packet: %v", err)
     	}
    +
    +	// don't process this packet further
    +	return true
     }
     
     // handleRoutedTraffic handles routed traffic.
    diff --git a/client/iface/device.go b/client/iface/device.go
    index 2a170adfb..86e9dab4b 100644
    --- a/client/iface/device.go
    +++ b/client/iface/device.go
    @@ -3,6 +3,8 @@
     package iface
     
     import (
    +	"golang.zx2c4.com/wireguard/tun/netstack"
    +
     	wgdevice "golang.zx2c4.com/wireguard/device"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
    @@ -18,4 +20,5 @@ type WGTunDevice interface {
     	Close() error
     	FilteredDevice() *device.FilteredDevice
     	Device() *wgdevice.Device
    +	GetNet() *netstack.Net
     }
    diff --git a/client/iface/device/device_android.go b/client/iface/device/device_android.go
    index 772722b83..55081e181 100644
    --- a/client/iface/device/device_android.go
    +++ b/client/iface/device/device_android.go
    @@ -9,6 +9,7 @@ import (
     	"golang.org/x/sys/unix"
     	"golang.zx2c4.com/wireguard/device"
     	"golang.zx2c4.com/wireguard/tun"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    @@ -130,6 +131,10 @@ func (t *WGTunDevice) FilteredDevice() *FilteredDevice {
     	return t.filteredDevice
     }
     
    +func (t *WGTunDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    +
     func routesToString(routes []string) string {
     	return strings.Join(routes, ";")
     }
    diff --git a/client/iface/device/device_darwin.go b/client/iface/device/device_darwin.go
    index fe7ed1752..1a5635ff2 100644
    --- a/client/iface/device/device_darwin.go
    +++ b/client/iface/device/device_darwin.go
    @@ -9,6 +9,7 @@ import (
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/device"
     	"golang.zx2c4.com/wireguard/tun"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    @@ -143,3 +144,7 @@ func (t *TunDevice) assignAddr() error {
     	}
     	return nil
     }
    +
    +func (t *TunDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    diff --git a/client/iface/device/device_ios.go b/client/iface/device/device_ios.go
    index cdabd2c85..b106d475c 100644
    --- a/client/iface/device/device_ios.go
    +++ b/client/iface/device/device_ios.go
    @@ -10,6 +10,7 @@ import (
     	"golang.org/x/sys/unix"
     	"golang.zx2c4.com/wireguard/device"
     	"golang.zx2c4.com/wireguard/tun"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    @@ -131,3 +132,7 @@ func (t *TunDevice) UpdateAddr(addr WGAddress) error {
     func (t *TunDevice) FilteredDevice() *FilteredDevice {
     	return t.filteredDevice
     }
    +
    +func (t *TunDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    diff --git a/client/iface/device/device_kernel_unix.go b/client/iface/device/device_kernel_unix.go
    index 3314b576b..fe1d1147f 100644
    --- a/client/iface/device/device_kernel_unix.go
    +++ b/client/iface/device/device_kernel_unix.go
    @@ -10,6 +10,7 @@ import (
     	"github.com/pion/transport/v3"
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    @@ -165,3 +166,7 @@ func (t *TunKernelDevice) FilteredDevice() *FilteredDevice {
     func (t *TunKernelDevice) assignAddr() error {
     	return t.link.assignAddr(t.address)
     }
    +
    +func (t *TunKernelDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    diff --git a/client/iface/device/device_netstack.go b/client/iface/device/device_netstack.go
    index c7d297187..0cb02fd19 100644
    --- a/client/iface/device/device_netstack.go
    +++ b/client/iface/device/device_netstack.go
    @@ -8,10 +8,12 @@ import (
     
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    -	"github.com/netbirdio/netbird/client/iface/netstack"
    +	nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
    +	nbnet "github.com/netbirdio/netbird/util/net"
     )
     
     type TunNetstackDevice struct {
    @@ -25,9 +27,11 @@ type TunNetstackDevice struct {
     
     	device         *device.Device
     	filteredDevice *FilteredDevice
    -	nsTun          *netstack.NetStackTun
    +	nsTun          *nbnetstack.NetStackTun
     	udpMux         *bind.UniversalUDPMuxDefault
     	configurer     WGConfigurer
    +
    +	net *netstack.Net
     }
     
     func NewNetstackDevice(name string, address WGAddress, wgPort int, key string, mtu int, iceBind *bind.ICEBind, listenAddress string) *TunNetstackDevice {
    @@ -43,13 +47,19 @@ func NewNetstackDevice(name string, address WGAddress, wgPort int, key string, m
     }
     
     func (t *TunNetstackDevice) Create() (WGConfigurer, error) {
    -	log.Info("create netstack tun interface")
    -	t.nsTun = netstack.NewNetStackTun(t.listenAddress, t.address.IP.String(), t.mtu)
    -	tunIface, err := t.nsTun.Create()
    +	log.Info("create nbnetstack tun interface")
    +
    +	// TODO: get from service listener runtime IP
    +	dnsAddr := nbnet.GetLastIPFromNetwork(t.address.Network, 1)
    +	log.Debugf("netstack using address: %s", t.address.IP)
    +	t.nsTun = nbnetstack.NewNetStackTun(t.listenAddress, t.address.IP, dnsAddr, t.mtu)
    +	log.Debugf("netstack using dns address: %s", dnsAddr)
    +	tunIface, net, err := t.nsTun.Create()
     	if err != nil {
     		return nil, fmt.Errorf("error creating tun device: %s", err)
     	}
     	t.filteredDevice = newDeviceFilter(tunIface)
    +	t.net = net
     
     	t.device = device.NewDevice(
     		t.filteredDevice,
    @@ -122,3 +132,7 @@ func (t *TunNetstackDevice) FilteredDevice() *FilteredDevice {
     func (t *TunNetstackDevice) Device() *device.Device {
     	return t.device
     }
    +
    +func (t *TunNetstackDevice) GetNet() *netstack.Net {
    +	return t.net
    +}
    diff --git a/client/iface/device/device_usp_unix.go b/client/iface/device/device_usp_unix.go
    index 4ac87aecb..07570617a 100644
    --- a/client/iface/device/device_usp_unix.go
    +++ b/client/iface/device/device_usp_unix.go
    @@ -8,6 +8,7 @@ import (
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/device"
     	"golang.zx2c4.com/wireguard/tun"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    @@ -135,3 +136,7 @@ func (t *USPDevice) assignAddr() error {
     
     	return link.assignAddr(t.address)
     }
    +
    +func (t *USPDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    diff --git a/client/iface/device/device_windows.go b/client/iface/device/device_windows.go
    index e603d7696..0fd1b3326 100644
    --- a/client/iface/device/device_windows.go
    +++ b/client/iface/device/device_windows.go
    @@ -8,6 +8,7 @@ import (
     	"golang.org/x/sys/windows"
     	"golang.zx2c4.com/wireguard/device"
     	"golang.zx2c4.com/wireguard/tun"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
    @@ -174,3 +175,7 @@ func (t *TunDevice) assignAddr() error {
     	log.Debugf("adding address %s to interface: %s", t.address.IP, t.name)
     	return luid.SetIPAddresses([]netip.Prefix{netip.MustParsePrefix(t.address.String())})
     }
    +
    +func (t *TunDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    diff --git a/client/iface/device_android.go b/client/iface/device_android.go
    index 028f6fa7d..5cbeb70f8 100644
    --- a/client/iface/device_android.go
    +++ b/client/iface/device_android.go
    @@ -3,6 +3,8 @@ package iface
     import (
     	wgdevice "golang.zx2c4.com/wireguard/device"
     
    +	"golang.zx2c4.com/wireguard/tun/netstack"
    +
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/device"
     )
    @@ -16,4 +18,5 @@ type WGTunDevice interface {
     	Close() error
     	FilteredDevice() *device.FilteredDevice
     	Device() *wgdevice.Device
    +	GetNet() *netstack.Net
     }
    diff --git a/client/iface/iface.go b/client/iface/iface.go
    index 64219975f..8056dd9a6 100644
    --- a/client/iface/iface.go
    +++ b/client/iface/iface.go
    @@ -9,6 +9,7 @@ import (
     	"github.com/hashicorp/go-multierror"
     	"github.com/pion/transport/v3"
     	log "github.com/sirupsen/logrus"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	wgdevice "golang.zx2c4.com/wireguard/device"
    @@ -241,3 +242,11 @@ func (w *WGIface) waitUntilRemoved() error {
     		}
     	}
     }
    +
    +// GetNet returns the netstack.Net for the netstack device
    +func (w *WGIface) GetNet() *netstack.Net {
    +	w.mu.Lock()
    +	defer w.mu.Unlock()
    +
    +	return w.tun.GetNet()
    +}
    diff --git a/client/iface/iface_moc.go b/client/iface/iface_moc.go
    index 5f57bc821..f92a8cfc8 100644
    --- a/client/iface/iface_moc.go
    +++ b/client/iface/iface_moc.go
    @@ -5,6 +5,7 @@ import (
     	"time"
     
     	wgdevice "golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
    @@ -34,6 +35,7 @@ type MockWGIface struct {
     	GetStatsFunc               func(peerKey string) (configurer.WGStats, error)
     	GetInterfaceGUIDStringFunc func() (string, error)
     	GetProxyFunc               func() wgproxy.Proxy
    +	GetNetFunc                 func() *netstack.Net
     }
     
     func (m *MockWGIface) GetInterfaceGUIDString() (string, error) {
    @@ -115,3 +117,7 @@ func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) {
     func (m *MockWGIface) GetProxy() wgproxy.Proxy {
     	return m.GetProxyFunc()
     }
    +
    +func (m *MockWGIface) GetNet() *netstack.Net {
    +	return m.GetNetFunc()
    +}
    diff --git a/client/iface/iwginterface.go b/client/iface/iwginterface.go
    index 472ab45f9..2b919ac9e 100644
    --- a/client/iface/iwginterface.go
    +++ b/client/iface/iwginterface.go
    @@ -7,6 +7,7 @@ import (
     	"time"
     
     	wgdevice "golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
    @@ -35,4 +36,5 @@ type IWGIface interface {
     	GetDevice() *device.FilteredDevice
     	GetWGDevice() *wgdevice.Device
     	GetStats(peerKey string) (configurer.WGStats, error)
    +	GetNet() *netstack.Net
     }
    diff --git a/client/iface/iwginterface_windows.go b/client/iface/iwginterface_windows.go
    index c9183cafd..cac096b54 100644
    --- a/client/iface/iwginterface_windows.go
    +++ b/client/iface/iwginterface_windows.go
    @@ -5,6 +5,7 @@ import (
     	"time"
     
     	wgdevice "golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
    @@ -34,4 +35,5 @@ type IWGIface interface {
     	GetWGDevice() *wgdevice.Device
     	GetStats(peerKey string) (configurer.WGStats, error)
     	GetInterfaceGUIDString() (string, error)
    +	GetNet() *netstack.Net
     }
    diff --git a/client/iface/netstack/env.go b/client/iface/netstack/env.go
    index 09889a57e..cdbf975b1 100644
    --- a/client/iface/netstack/env.go
    +++ b/client/iface/netstack/env.go
    @@ -8,9 +8,11 @@ import (
     	log "github.com/sirupsen/logrus"
     )
     
    +const EnvUseNetstackMode = "NB_USE_NETSTACK_MODE"
    +
     // IsEnabled todo: move these function to cmd layer
     func IsEnabled() bool {
    -	return os.Getenv("NB_USE_NETSTACK_MODE") == "true"
    +	return os.Getenv(EnvUseNetstackMode) == "true"
     }
     
     func ListenAddr() string {
    diff --git a/client/iface/netstack/tun.go b/client/iface/netstack/tun.go
    index c180e4ef5..01f19875e 100644
    --- a/client/iface/netstack/tun.go
    +++ b/client/iface/netstack/tun.go
    @@ -1,15 +1,22 @@
     package netstack
     
     import (
    +	"fmt"
    +	"net"
     	"net/netip"
    +	"os"
    +	"strconv"
     
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/tun"
     	"golang.zx2c4.com/wireguard/tun/netstack"
     )
     
    +const EnvSkipProxy = "NB_NETSTACK_SKIP_PROXY"
    +
     type NetStackTun struct { //nolint:revive
    -	address       string
    +	address       net.IP
    +	dnsAddress    net.IP
     	mtu           int
     	listenAddress string
     
    @@ -17,29 +24,48 @@ type NetStackTun struct { //nolint:revive
     	tundev tun.Device
     }
     
    -func NewNetStackTun(listenAddress string, address string, mtu int) *NetStackTun {
    +func NewNetStackTun(listenAddress string, address net.IP, dnsAddress net.IP, mtu int) *NetStackTun {
     	return &NetStackTun{
     		address:       address,
    +		dnsAddress:    dnsAddress,
     		mtu:           mtu,
     		listenAddress: listenAddress,
     	}
     }
     
    -func (t *NetStackTun) Create() (tun.Device, error) {
    +func (t *NetStackTun) Create() (tun.Device, *netstack.Net, error) {
    +	addr, ok := netip.AddrFromSlice(t.address)
    +	if !ok {
    +		return nil, nil, fmt.Errorf("convert address to netip.Addr: %v", t.address)
    +	}
    +
    +	dnsAddr, ok := netip.AddrFromSlice(t.dnsAddress)
    +	if !ok {
    +		return nil, nil, fmt.Errorf("convert dns address to netip.Addr: %v", t.dnsAddress)
    +	}
    +
     	nsTunDev, tunNet, err := netstack.CreateNetTUN(
    -		[]netip.Addr{netip.MustParseAddr(t.address)},
    -		[]netip.Addr{},
    +		[]netip.Addr{addr.Unmap()},
    +		[]netip.Addr{dnsAddr.Unmap()},
     		t.mtu)
     	if err != nil {
    -		return nil, err
    +		return nil, nil, err
     	}
     	t.tundev = nsTunDev
     
    +	skipProxy, err := strconv.ParseBool(os.Getenv(EnvSkipProxy))
    +	if err != nil {
    +		log.Errorf("failed to parse NB_ETSTACK_SKIP_PROXY: %s", err)
    +	}
    +	if skipProxy {
    +		return nsTunDev, tunNet, nil
    +	}
    +
     	dialer := NewNSDialer(tunNet)
     	t.proxy, err = NewSocks5(dialer)
     	if err != nil {
     		_ = t.tundev.Close()
    -		return nil, err
    +		return nil, nil, err
     	}
     
     	go func() {
    @@ -49,7 +75,7 @@ func (t *NetStackTun) Create() (tun.Device, error) {
     		}
     	}()
     
    -	return nsTunDev, nil
    +	return nsTunDev, tunNet, nil
     }
     
     func (t *NetStackTun) Close() error {
    diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go
    index 729b90cc0..250f3ab2e 100644
    --- a/client/internal/dns/service_memory.go
    +++ b/client/internal/dns/service_memory.go
    @@ -2,7 +2,6 @@ package dns
     
     import (
     	"fmt"
    -	"math/big"
     	"net"
     	"sync"
     
    @@ -10,6 +9,8 @@ import (
     	"github.com/google/gopacket/layers"
     	"github.com/miekg/dns"
     	log "github.com/sirupsen/logrus"
    +
    +	nbnet "github.com/netbirdio/netbird/util/net"
     )
     
     type ServiceViaMemory struct {
    @@ -27,7 +28,7 @@ func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory {
     		wgInterface: wgIface,
     		dnsMux:      dns.NewServeMux(),
     
    -		runtimeIP:   getLastIPFromNetwork(wgIface.Address().Network, 1),
    +		runtimeIP:   nbnet.GetLastIPFromNetwork(wgIface.Address().Network, 1).String(),
     		runtimePort: defaultPort,
     	}
     	return s
    @@ -118,22 +119,3 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) {
     
     	return filter.AddUDPPacketHook(false, net.ParseIP(s.runtimeIP), uint16(s.runtimePort), hook), nil
     }
    -
    -func getLastIPFromNetwork(network *net.IPNet, fromEnd int) string {
    -	// Calculate the last IP in the CIDR range
    -	var endIP net.IP
    -	for i := 0; i < len(network.IP); i++ {
    -		endIP = append(endIP, network.IP[i]|^network.Mask[i])
    -	}
    -
    -	// convert to big.Int
    -	endInt := big.NewInt(0)
    -	endInt.SetBytes(endIP)
    -
    -	// subtract fromEnd from the last ip
    -	fromEndBig := big.NewInt(int64(fromEnd))
    -	resultInt := big.NewInt(0)
    -	resultInt.Sub(endInt, fromEndBig)
    -
    -	return net.IP(resultInt.Bytes()).String()
    -}
    diff --git a/client/internal/dns/service_memory_test.go b/client/internal/dns/service_memory_test.go
    index bea4f4ce8..244adfaef 100644
    --- a/client/internal/dns/service_memory_test.go
    +++ b/client/internal/dns/service_memory_test.go
    @@ -3,6 +3,8 @@ package dns
     import (
     	"net"
     	"testing"
    +
    +	nbnet "github.com/netbirdio/netbird/util/net"
     )
     
     func TestGetLastIPFromNetwork(t *testing.T) {
    @@ -23,7 +25,7 @@ func TestGetLastIPFromNetwork(t *testing.T) {
     			return
     		}
     
    -		lastIP := getLastIPFromNetwork(ipnet, 1)
    +		lastIP := nbnet.GetLastIPFromNetwork(ipnet, 1).String()
     		if lastIP != tt.ip {
     			t.Errorf("wrong IP address, expected %s: got %s", tt.ip, lastIP)
     		}
    diff --git a/client/internal/engine.go b/client/internal/engine.go
    index 14e0d348f..d590c0db6 100644
    --- a/client/internal/engine.go
    +++ b/client/internal/engine.go
    @@ -19,6 +19,7 @@ import (
     	"github.com/pion/ice/v3"
     	"github.com/pion/stun/v2"
     	log "github.com/sirupsen/logrus"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     	"google.golang.org/protobuf/proto"
     
    @@ -28,7 +29,7 @@ import (
     	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/device"
    -	"github.com/netbirdio/netbird/client/iface/netstack"
    +	nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
     	"github.com/netbirdio/netbird/client/internal/acl"
     	"github.com/netbirdio/netbird/client/internal/dns"
     	"github.com/netbirdio/netbird/client/internal/dnsfwd"
    @@ -724,7 +725,7 @@ func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error {
     			// start SSH server if it wasn't running
     			if isNil(e.sshServer) {
     				listenAddr := fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort)
    -				if netstack.IsEnabled() {
    +				if nbnetstack.IsEnabled() {
     					listenAddr = fmt.Sprintf("127.0.0.1:%d", nbssh.DefaultSSHPort)
     				}
     				// nil sshServer means it has not yet been started
    @@ -1716,6 +1717,37 @@ func (e *Engine) updateDNSForwarder(enabled bool, domains []string) {
     	}
     }
     
    +func (e *Engine) GetNet() (*netstack.Net, error) {
    +	e.syncMsgMux.Lock()
    +	intf := e.wgInterface
    +	e.syncMsgMux.Unlock()
    +	if intf == nil {
    +		return nil, errors.New("wireguard interface not initialized")
    +	}
    +
    +	nsnet := intf.GetNet()
    +	if nsnet == nil {
    +		return nil, errors.New("failed to get netstack")
    +	}
    +	return nsnet, nil
    +}
    +
    +func (e *Engine) Address() (netip.Addr, error) {
    +	e.syncMsgMux.Lock()
    +	intf := e.wgInterface
    +	e.syncMsgMux.Unlock()
    +	if intf == nil {
    +		return netip.Addr{}, errors.New("wireguard interface not initialized")
    +	}
    +
    +	addr := e.wgInterface.Address()
    +	ip, ok := netip.AddrFromSlice(addr.IP)
    +	if !ok {
    +		return netip.Addr{}, errors.New("failed to convert address to netip.Addr")
    +	}
    +	return ip.Unmap(), nil
    +}
    +
     // isChecksEqual checks if two slices of checks are equal.
     func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool {
     	for _, check := range checks {
    diff --git a/util/net/net.go b/util/net/net.go
    index 403aa87e7..7b43b952f 100644
    --- a/util/net/net.go
    +++ b/util/net/net.go
    @@ -1,6 +1,7 @@
     package net
     
     import (
    +	"math/big"
     	"net"
     
     	"github.com/google/uuid"
    @@ -26,3 +27,22 @@ type RemoveHookFunc func(connID ConnectionID) error
     func GenerateConnID() ConnectionID {
     	return ConnectionID(uuid.NewString())
     }
    +
    +func GetLastIPFromNetwork(network *net.IPNet, fromEnd int) net.IP {
    +	// Calculate the last IP in the CIDR range
    +	var endIP net.IP
    +	for i := 0; i < len(network.IP); i++ {
    +		endIP = append(endIP, network.IP[i]|^network.Mask[i])
    +	}
    +
    +	// convert to big.Int
    +	endInt := big.NewInt(0)
    +	endInt.SetBytes(endIP)
    +
    +	// subtract fromEnd from the last ip
    +	fromEndBig := big.NewInt(int64(fromEnd))
    +	resultInt := big.NewInt(0)
    +	resultInt.Sub(endInt, fromEndBig)
    +
    +	return resultInt.Bytes()
    +}
    
    From d7d5b1b1d608ac7a072bc7106f8b6dc27c80d9e6 Mon Sep 17 00:00:00 2001
    From: Viktor Liu <17948409+lixmal@users.noreply.github.com>
    Date: Thu, 20 Feb 2025 15:01:53 +0100
    Subject: [PATCH 16/23] Skip CLI session expired notifcation if notifications
     are disabled (#3266)
    
    ---
     client/server/server.go | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    diff --git a/client/server/server.go b/client/server/server.go
    index e4e2c8f6f..2efbb94ff 100644
    --- a/client/server/server.go
    +++ b/client/server/server.go
    @@ -766,10 +766,11 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto
     		DisableNotifications: s.config.DisableNotifications,
     	}, nil
     }
    +
     func (s *Server) onSessionExpire() {
     	if runtime.GOOS != "windows" {
     		isUIActive := internal.CheckUIApp()
    -		if !isUIActive {
    +		if !isUIActive && !s.config.DisableNotifications {
     			if err := sendTerminalNotification(); err != nil {
     				log.Errorf("send session expire terminal notification: %v", err)
     			}
    
    From 77e40f41f24ce0b2c50adfc04f543f71262c4ff9 Mon Sep 17 00:00:00 2001
    From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com>
    Date: Thu, 20 Feb 2025 20:24:40 +0000
    Subject: [PATCH 17/23] [management] refactor auth (#3296)
    
    ---
     client/cmd/testutil_test.go                   |   2 +-
     client/internal/engine_test.go                |   2 +-
     client/server/server_test.go                  |   2 +-
     go.mod                                        |   2 +-
     go.sum                                        |   4 +-
     management/client/client_test.go              |   2 +-
     management/cmd/management.go                  |  28 +-
     management/server/account.go                  | 314 ++++----------
     management/server/account_test.go             | 231 +++-------
     .../{jwtclaims => auth/jwt}/extractor.go      | 132 +++---
     management/server/auth/jwt/validator.go       | 302 +++++++++++++
     management/server/auth/manager.go             | 170 ++++++++
     management/server/auth/manager_mock.go        |  54 +++
     management/server/auth/manager_test.go        | 407 ++++++++++++++++++
     management/server/auth/test_data/jwks.json    |  11 +
     management/server/auth/test_data/sample_key   |  27 ++
     .../server/auth/test_data/sample_key.pub      |   9 +
     management/server/config.go                   |   7 -
     management/server/context/auth.go             |  60 +++
     management/server/grpcserver.go               |  56 +--
     management/server/http/handler.go             |  63 ++-
     .../handlers/accounts/accounts_handler.go     |  35 +-
     .../accounts/accounts_handler_test.go         |  23 +-
     .../http/handlers/dns/dns_settings_handler.go |  36 +-
     .../handlers/dns/dns_settings_handler_test.go |  20 +-
     .../http/handlers/dns/nameservers_handler.go  |  48 +--
     .../handlers/dns/nameservers_handler_test.go  |  20 +-
     .../http/handlers/events/events_handler.go    |  25 +-
     .../handlers/events/events_handler_test.go    |  20 +-
     .../http/handlers/groups/groups_handler.go    |  43 +-
     .../handlers/groups/groups_handler_test.go    |  30 +-
     .../server/http/handlers/networks/handler.go  |  53 +--
     .../handlers/networks/resources_handler.go    |  51 +--
     .../http/handlers/networks/routers_handler.go |  45 +-
     .../http/handlers/peers/peers_handler.go      |  31 +-
     .../http/handlers/peers/peers_handler_test.go |  38 +-
     .../policies/geolocation_handler_test.go      |  24 +-
     .../handlers/policies/geolocations_handler.go |  19 +-
     .../handlers/policies/policies_handler.go     |  42 +-
     .../policies/policies_handler_test.go         |  24 +-
     .../policies/posture_checks_handler.go        |  38 +-
     .../policies/posture_checks_handler_test.go   |  24 +-
     .../http/handlers/routes/routes_handler.go    |  39 +-
     .../handlers/routes/routes_handler_test.go    |  48 +--
     .../handlers/setup_keys/setupkeys_handler.go  |  36 +-
     .../setup_keys/setupkeys_handler_test.go      |  25 +-
     .../server/http/handlers/users/pat_handler.go |  32 +-
     .../http/handlers/users/pat_handler_test.go   |  23 +-
     .../http/handlers/users/users_handler.go      |  46 +-
     .../http/handlers/users/users_handler_test.go |  41 +-
     .../server/http/middleware/access_control.go  |  28 +-
     .../server/http/middleware/auth_middleware.go | 161 +++----
     .../http/middleware/auth_middleware_test.go   | 176 ++++++--
     .../peers_handler_benchmark_test.go           |  20 +-
     .../users_handler_benchmark_test.go           |  44 +-
     .../http/testing/testing_tools/tools.go       |  41 +-
     management/server/jwtclaims/claims.go         |  19 -
     management/server/jwtclaims/extractor_test.go | 227 ----------
     management/server/jwtclaims/jwtValidator.go   | 349 ---------------
     management/server/management_proto_test.go    |   2 +-
     management/server/management_test.go          |   1 +
     management/server/mock_server/account_mock.go |  55 +--
     management/server/user.go                     |  25 +-
     management/server/user_test.go                |  10 +-
     64 files changed, 2085 insertions(+), 1937 deletions(-)
     rename management/server/{jwtclaims => auth/jwt}/extractor.go (51%)
     create mode 100644 management/server/auth/jwt/validator.go
     create mode 100644 management/server/auth/manager.go
     create mode 100644 management/server/auth/manager_mock.go
     create mode 100644 management/server/auth/manager_test.go
     create mode 100644 management/server/auth/test_data/jwks.json
     create mode 100644 management/server/auth/test_data/sample_key
     create mode 100644 management/server/auth/test_data/sample_key.pub
     create mode 100644 management/server/context/auth.go
     delete mode 100644 management/server/jwtclaims/claims.go
     delete mode 100644 management/server/jwtclaims/extractor_test.go
     delete mode 100644 management/server/jwtclaims/jwtValidator.go
    
    diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go
    index e3e644357..e0d784048 100644
    --- a/client/cmd/testutil_test.go
    +++ b/client/cmd/testutil_test.go
    @@ -95,7 +95,7 @@ func startManagement(t *testing.T, config *mgmt.Config, testFile string) (*grpc.
     	}
     
     	secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
    -	mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
    +	mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil)
     	if err != nil {
     		t.Fatal(err)
     	}
    diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go
    index ca49eca09..e32e262b9 100644
    --- a/client/internal/engine_test.go
    +++ b/client/internal/engine_test.go
    @@ -1226,7 +1226,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
     	}
     
     	secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
    -	mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
    +	mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil)
     	if err != nil {
     		return nil, "", err
     	}
    diff --git a/client/server/server_test.go b/client/server/server_test.go
    index 128de8e02..d6b651a79 100644
    --- a/client/server/server_test.go
    +++ b/client/server/server_test.go
    @@ -134,7 +134,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
     	}
     
     	secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
    -	mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
    +	mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil)
     	if err != nil {
     		return nil, "", err
     	}
    diff --git a/go.mod b/go.mod
    index 3e1208e5a..25e5dd1d2 100644
    --- a/go.mod
    +++ b/go.mod
    @@ -60,7 +60,7 @@ require (
     	github.com/miekg/dns v1.1.59
     	github.com/mitchellh/hashstructure/v2 v2.0.2
     	github.com/nadoo/ipset v0.5.0
    -	github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6
    +	github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc
     	github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d
     	github.com/okta/okta-sdk-golang/v2 v2.18.0
     	github.com/oschwald/maxminddb-golang v1.12.0
    diff --git a/go.sum b/go.sum
    index 54b77dbee..4057517d3 100644
    --- a/go.sum
    +++ b/go.sum
    @@ -529,8 +529,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S
     github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ=
     github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c=
     github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q=
    -github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6 h1:I/ODkZ8rSDOzlJbhEjD2luSI71zl+s5JgNvFHY0+mBU=
    -github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6/go.mod h1:izUUs1NT7ja+PwSX3kJ7ox8Kkn478tboBJSjL4kU6J0=
    +github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc h1:18xvjOy2tZVIK7rihNpf9DF/3mAiljYKWaQlWa9vJgI=
    +github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc/go.mod h1:izUUs1NT7ja+PwSX3kJ7ox8Kkn478tboBJSjL4kU6J0=
     github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8=
     github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
     github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d h1:bRq5TKgC7Iq20pDiuC54yXaWnAVeS5PdGpSokFTlR28=
    diff --git a/management/client/client_test.go b/management/client/client_test.go
    index 6ef5df163..2bf802821 100644
    --- a/management/client/client_test.go
    +++ b/management/client/client_test.go
    @@ -78,7 +78,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
     	}
     
     	secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
    -	mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
    +	mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil)
     	if err != nil {
     		t.Fatal(err)
     	}
    diff --git a/management/cmd/management.go b/management/cmd/management.go
    index 1c8fca8dc..9712f04aa 100644
    --- a/management/cmd/management.go
    +++ b/management/cmd/management.go
    @@ -39,13 +39,12 @@ import (
     	"github.com/netbirdio/netbird/formatter"
     	mgmtProto "github.com/netbirdio/netbird/management/proto"
     	"github.com/netbirdio/netbird/management/server"
    +	"github.com/netbirdio/netbird/management/server/auth"
     	nbContext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/groups"
     	nbhttp "github.com/netbirdio/netbird/management/server/http"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/idp"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/metrics"
     	"github.com/netbirdio/netbird/management/server/networks"
     	"github.com/netbirdio/netbird/management/server/networks/resources"
    @@ -255,24 +254,13 @@ var (
     				tlsEnabled = true
     			}
     
    -			jwtValidator, err := jwtclaims.NewJWTValidator(
    -				ctx,
    +			authManager := auth.NewManager(store,
     				config.HttpConfig.AuthIssuer,
    -				config.GetAuthAudiences(),
    +				config.HttpConfig.AuthAudience,
     				config.HttpConfig.AuthKeysLocation,
    -				config.HttpConfig.IdpSignKeyRefreshEnabled,
    -			)
    -			if err != nil {
    -				return fmt.Errorf("failed creating JWT validator: %v", err)
    -			}
    -
    -			httpAPIAuthCfg := configs.AuthCfg{
    -				Issuer:       config.HttpConfig.AuthIssuer,
    -				Audience:     config.HttpConfig.AuthAudience,
    -				UserIDClaim:  config.HttpConfig.AuthUserIDClaim,
    -				KeysLocation: config.HttpConfig.AuthKeysLocation,
    -			}
    -
    +				config.HttpConfig.AuthUserIDClaim,
    +				config.GetAuthAudiences(),
    +				config.HttpConfig.IdpSignKeyRefreshEnabled)
     			userManager := users.NewManager(store)
     			settingsManager := settings.NewManager(store)
     			permissionsManager := permissions.NewManager(userManager, settingsManager)
    @@ -281,7 +269,7 @@ var (
     			routersManager := routers.NewManager(store, permissionsManager, accountManager)
     			networksManager := networks.NewManager(store, permissionsManager, resourcesManager, routersManager, accountManager)
     
    -			httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, jwtValidator, appMetrics, httpAPIAuthCfg, integratedPeerValidator)
    +			httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, authManager, appMetrics, config, integratedPeerValidator)
     			if err != nil {
     				return fmt.Errorf("failed creating HTTP API handler: %v", err)
     			}
    @@ -290,7 +278,7 @@ var (
     			ephemeralManager.LoadInitialPeers(ctx)
     
     			gRPCAPIHandler := grpc.NewServer(gRPCOpts...)
    -			srv, err := server.NewServer(ctx, config, accountManager, settingsManager, peersUpdateManager, secretsManager, appMetrics, ephemeralManager)
    +			srv, err := server.NewServer(ctx, config, accountManager, settingsManager, peersUpdateManager, secretsManager, appMetrics, ephemeralManager, authManager)
     			if err != nil {
     				return fmt.Errorf("failed creating gRPC API handler: %v", err)
     			}
    diff --git a/management/server/account.go b/management/server/account.go
    index 661569418..76c984286 100644
    --- a/management/server/account.go
    +++ b/management/server/account.go
    @@ -2,11 +2,8 @@ package server
     
     import (
     	"context"
    -	"crypto/sha256"
    -	b64 "encoding/base64"
     	"errors"
     	"fmt"
    -	"hash/crc32"
     	"math/rand"
     	"net"
     	"net/netip"
    @@ -24,14 +21,13 @@ import (
     	log "github.com/sirupsen/logrus"
     	"golang.org/x/exp/maps"
     
    -	"github.com/netbirdio/netbird/base62"
     	nbdns "github.com/netbirdio/netbird/dns"
     	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/server/activity"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/idp"
     	"github.com/netbirdio/netbird/management/server/integrated_validator"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/status"
    @@ -77,13 +73,10 @@ type AccountManager interface {
     	GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error)
     	AccountExists(ctx context.Context, accountID string) (bool, error)
     	GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error)
    -	GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
    -	CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error
    -	GetPATInfo(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error)
    +	GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
     	DeleteAccount(ctx context.Context, accountID, userID string) error
    -	MarkPATUsed(ctx context.Context, tokenID string) error
     	GetUserByID(ctx context.Context, id string) (*types.User, error)
    -	GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error)
    +	GetUserFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
     	ListUsers(ctx context.Context, accountID string) ([]*types.User, error)
     	GetPeers(ctx context.Context, accountID, userID string) ([]*nbpeer.Peer, error)
     	MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, accountID string) error
    @@ -150,6 +143,7 @@ type AccountManager interface {
     	DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error
     	UpdateAccountPeers(ctx context.Context, accountID string)
     	BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error)
    +	SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error
     }
     
     type DefaultAccountManager struct {
    @@ -954,11 +948,11 @@ func (am *DefaultAccountManager) removeUserFromCache(ctx context.Context, accoun
     }
     
     // updateAccountDomainAttributesIfNotUpToDate updates the account domain attributes if they are not up to date and then, saves the account changes
    -func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx context.Context, accountID string, claims jwtclaims.AuthorizationClaims,
    +func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx context.Context, accountID string, userAuth nbcontext.UserAuth,
     	primaryDomain bool,
     ) error {
    -	if claims.Domain == "" {
    -		log.WithContext(ctx).Errorf("claims don't contain a valid domain, skipping domain attributes update. Received claims: %v", claims)
    +	if userAuth.Domain == "" {
    +		log.WithContext(ctx).Errorf("claims don't contain a valid domain, skipping domain attributes update. Received claims: %v", userAuth)
     		return nil
     	}
     
    @@ -971,11 +965,11 @@ func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx
     		return err
     	}
     
    -	if domainIsUpToDate(accountDomain, domainCategory, claims) {
    +	if domainIsUpToDate(accountDomain, domainCategory, userAuth) {
     		return nil
     	}
     
    -	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId)
    +	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if err != nil {
     		log.WithContext(ctx).Errorf("error getting user: %v", err)
     		return err
    @@ -984,13 +978,13 @@ func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx
     	newDomain := accountDomain
     	newCategoty := domainCategory
     
    -	lowerDomain := strings.ToLower(claims.Domain)
    +	lowerDomain := strings.ToLower(userAuth.Domain)
     	if accountDomain != lowerDomain && user.HasAdminPower() {
     		newDomain = lowerDomain
     	}
     
     	if accountDomain == lowerDomain {
    -		newCategoty = claims.DomainCategory
    +		newCategoty = userAuth.DomainCategory
     	}
     
     	return am.Store.UpdateAccountDomainAttributes(ctx, accountID, newDomain, newCategoty, primaryDomain)
    @@ -1006,16 +1000,16 @@ func (am *DefaultAccountManager) handleExistingUserAccount(
     	ctx context.Context,
     	userAccountID string,
     	domainAccountID string,
    -	claims jwtclaims.AuthorizationClaims,
    +	userAuth nbcontext.UserAuth,
     ) error {
     	primaryDomain := domainAccountID == "" || userAccountID == domainAccountID
    -	err := am.updateAccountDomainAttributesIfNotUpToDate(ctx, userAccountID, claims, primaryDomain)
    +	err := am.updateAccountDomainAttributesIfNotUpToDate(ctx, userAccountID, userAuth, primaryDomain)
     	if err != nil {
     		return err
     	}
     
     	// we should register the account ID to this user's metadata in our IDP manager
    -	err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, userAccountID)
    +	err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, userAccountID)
     	if err != nil {
     		return err
     	}
    @@ -1025,20 +1019,20 @@ func (am *DefaultAccountManager) handleExistingUserAccount(
     
     // addNewPrivateAccount validates if there is an existing primary account for the domain, if so it adds the new user to that account,
     // otherwise it will create a new account and make it primary account for the domain.
    -func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domainAccountID string, claims jwtclaims.AuthorizationClaims) (string, error) {
    -	if claims.UserId == "" {
    +func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domainAccountID string, userAuth nbcontext.UserAuth) (string, error) {
    +	if userAuth.UserId == "" {
     		return "", fmt.Errorf("user ID is empty")
     	}
     
    -	lowerDomain := strings.ToLower(claims.Domain)
    +	lowerDomain := strings.ToLower(userAuth.Domain)
     
    -	newAccount, err := am.newAccount(ctx, claims.UserId, lowerDomain)
    +	newAccount, err := am.newAccount(ctx, userAuth.UserId, lowerDomain)
     	if err != nil {
     		return "", err
     	}
     
     	newAccount.Domain = lowerDomain
    -	newAccount.DomainCategory = claims.DomainCategory
    +	newAccount.DomainCategory = userAuth.DomainCategory
     	newAccount.IsDomainPrimaryAccount = true
     
     	err = am.Store.SaveAccount(ctx, newAccount)
    @@ -1046,33 +1040,33 @@ func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domai
     		return "", err
     	}
     
    -	err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, newAccount.Id)
    +	err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, newAccount.Id)
     	if err != nil {
     		return "", err
     	}
     
    -	am.StoreEvent(ctx, claims.UserId, claims.UserId, newAccount.Id, activity.UserJoined, nil)
    +	am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, newAccount.Id, activity.UserJoined, nil)
     
     	return newAccount.Id, nil
     }
     
    -func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, claims jwtclaims.AuthorizationClaims) (string, error) {
    +func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, userAuth nbcontext.UserAuth) (string, error) {
     	unlockAccount := am.Store.AcquireWriteLockByUID(ctx, domainAccountID)
     	defer unlockAccount()
     
    -	newUser := types.NewRegularUser(claims.UserId)
    +	newUser := types.NewRegularUser(userAuth.UserId)
     	newUser.AccountID = domainAccountID
     	err := am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser)
     	if err != nil {
     		return "", err
     	}
     
    -	err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, domainAccountID)
    +	err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, domainAccountID)
     	if err != nil {
     		return "", err
     	}
     
    -	am.StoreEvent(ctx, claims.UserId, claims.UserId, domainAccountID, activity.UserJoined, nil)
    +	am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, domainAccountID, activity.UserJoined, nil)
     
     	return domainAccountID, nil
     }
    @@ -1112,76 +1106,11 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str
     	return nil
     }
     
    -// MarkPATUsed marks a personal access token as used
    -func (am *DefaultAccountManager) MarkPATUsed(ctx context.Context, tokenID string) error {
    -	return am.Store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID)
    -}
    -
     // GetAccount returns an account associated with this account ID.
     func (am *DefaultAccountManager) GetAccount(ctx context.Context, accountID string) (*types.Account, error) {
     	return am.Store.GetAccount(ctx, accountID)
     }
     
    -// GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token.
    -func (am *DefaultAccountManager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
    -	user, pat, err = am.extractPATFromToken(ctx, token)
    -	if err != nil {
    -		return nil, nil, "", "", err
    -	}
    -
    -	domain, category, err = am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID)
    -	if err != nil {
    -		return nil, nil, "", "", err
    -	}
    -
    -	return user, pat, domain, category, nil
    -}
    -
    -// extractPATFromToken validates the token structure and retrieves associated User and PAT.
    -func (am *DefaultAccountManager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) {
    -	if len(token) != types.PATLength {
    -		return nil, nil, fmt.Errorf("token has incorrect length")
    -	}
    -
    -	prefix := token[:len(types.PATPrefix)]
    -	if prefix != types.PATPrefix {
    -		return nil, nil, fmt.Errorf("token has wrong prefix")
    -	}
    -	secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength]
    -	encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength]
    -
    -	verificationChecksum, err := base62.Decode(encodedChecksum)
    -	if err != nil {
    -		return nil, nil, fmt.Errorf("token checksum decoding failed: %w", err)
    -	}
    -
    -	secretChecksum := crc32.ChecksumIEEE([]byte(secret))
    -	if secretChecksum != verificationChecksum {
    -		return nil, nil, fmt.Errorf("token checksum does not match")
    -	}
    -
    -	hashedToken := sha256.Sum256([]byte(token))
    -	encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:])
    -
    -	var user *types.User
    -	var pat *types.PersonalAccessToken
    -
    -	err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
    -		pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken)
    -		if err != nil {
    -			return err
    -		}
    -
    -		user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID)
    -		return err
    -	})
    -	if err != nil {
    -		return nil, nil, err
    -	}
    -
    -	return user, pat, nil
    -}
    -
     // GetAccountByID returns an account associated with this account ID.
     func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) {
     	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
    @@ -1196,58 +1125,56 @@ func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID s
     	return am.Store.GetAccount(ctx, accountID)
     }
     
    -// GetAccountIDFromToken returns an account ID associated with this token.
    -func (am *DefaultAccountManager) GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -	if claims.UserId == "" {
    +func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
    +	if userAuth.UserId == "" {
     		return "", "", errors.New(emptyUserID)
     	}
     	if am.singleAccountMode && am.singleAccountModeDomain != "" {
     		// This section is mostly related to self-hosted installations.
     		// We override incoming domain claims to group users under a single account.
    -		claims.Domain = am.singleAccountModeDomain
    -		claims.DomainCategory = types.PrivateCategory
    +		userAuth.Domain = am.singleAccountModeDomain
    +		userAuth.DomainCategory = types.PrivateCategory
     		log.WithContext(ctx).Debugf("overriding JWT Domain and DomainCategory claims since single account mode is enabled")
     	}
     
    -	accountID, err := am.getAccountIDWithAuthorizationClaims(ctx, claims)
    +	accountID, err := am.getAccountIDWithAuthorizationClaims(ctx, userAuth)
     	if err != nil {
     		return "", "", err
     	}
     
    -	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId)
    +	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if err != nil {
     		// this is not really possible because we got an account by user ID
    -		return "", "", status.Errorf(status.NotFound, "user %s not found", claims.UserId)
    +		return "", "", status.Errorf(status.NotFound, "user %s not found", userAuth.UserId)
    +	}
    +
    +	if userAuth.IsChild {
    +		return accountID, user.Id, nil
     	}
     
     	if user.AccountID != accountID {
    -		return "", "", status.Errorf(status.PermissionDenied, "user %s is not part of the account %s", claims.UserId, accountID)
    +		return "", "", status.Errorf(status.PermissionDenied, "user %s is not part of the account %s", userAuth.UserId, accountID)
     	}
     
    -	if !user.IsServiceUser && claims.Invited {
    +	if !user.IsServiceUser && userAuth.Invited {
     		err = am.redeemInvite(ctx, accountID, user.Id)
     		if err != nil {
     			return "", "", err
     		}
     	}
     
    -	if err = am.syncJWTGroups(ctx, accountID, claims); err != nil {
    -		return "", "", err
    -	}
    -
     	return accountID, user.Id, nil
     }
     
     // syncJWTGroups processes the JWT groups for a user, updates the account based on the groups,
     // and propagates changes to peers if group propagation is enabled.
    -func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID string, claims jwtclaims.AuthorizationClaims) error {
    -	if claim, exists := claims.Raw[jwtclaims.IsToken]; exists {
    -		if isToken, ok := claim.(bool); ok && isToken {
    -			return nil
    -		}
    +// requires userAuth to have been ValidateAndParseToken and EnsureUserAccessByJWTGroups by the AuthManager
    +func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error {
    +	if userAuth.IsChild || userAuth.IsPAT {
    +		return nil
     	}
     
    -	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
    +	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, userAuth.AccountId)
     	if err != nil {
     		return err
     	}
    @@ -1261,9 +1188,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     		return nil
     	}
     
    -	jwtGroupsNames := extractJWTGroups(ctx, settings.JWTGroupsClaimName, claims)
    -
    -	unlockAccount := am.Store.AcquireWriteLockByUID(ctx, accountID)
    +	unlockAccount := am.Store.AcquireWriteLockByUID(ctx, userAuth.AccountId)
     	defer func() {
     		if unlockAccount != nil {
     			unlockAccount()
    @@ -1275,17 +1200,17 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     	var hasChanges bool
     	var user *types.User
     	err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
    -		user, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId)
    +		user, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     		if err != nil {
     			return fmt.Errorf("error getting user: %w", err)
     		}
     
    -		groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID)
    +		groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, userAuth.AccountId)
     		if err != nil {
     			return fmt.Errorf("error getting account groups: %w", err)
     		}
     
    -		changed, updatedAutoGroups, newGroupsToCreate, err := am.getJWTGroupsChanges(user, groups, jwtGroupsNames)
    +		changed, updatedAutoGroups, newGroupsToCreate, err := am.getJWTGroupsChanges(user, groups, userAuth.Groups)
     		if err != nil {
     			return fmt.Errorf("error getting JWT groups changes: %w", err)
     		}
    @@ -1310,7 +1235,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     
     		// Propagate changes to peers if group propagation is enabled
     		if settings.GroupsPropagationEnabled {
    -			groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID)
    +			groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, userAuth.AccountId)
     			if err != nil {
     				return fmt.Errorf("error getting account groups: %w", err)
     			}
    @@ -1320,7 +1245,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     				groupsMap[group.ID] = group
     			}
     
    -			peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, claims.UserId)
    +			peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, userAuth.AccountId, userAuth.UserId)
     			if err != nil {
     				return fmt.Errorf("error getting user peers: %w", err)
     			}
    @@ -1334,7 +1259,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     				return fmt.Errorf("error saving groups: %w", err)
     			}
     
    -			if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil {
    +			if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, userAuth.AccountId); err != nil {
     				return fmt.Errorf("error incrementing network serial: %w", err)
     			}
     		}
    @@ -1352,45 +1277,45 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     	}
     
     	for _, g := range addNewGroups {
    -		group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, accountID, g)
    +		group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, userAuth.AccountId, g)
     		if err != nil {
    -			log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, accountID)
    +			log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, userAuth.AccountId)
     		} else {
     			meta := map[string]any{
     				"group": group.Name, "group_id": group.ID,
     				"is_service_user": user.IsServiceUser, "user_name": user.ServiceUserName,
     			}
    -			am.StoreEvent(ctx, user.Id, user.Id, accountID, activity.GroupAddedToUser, meta)
    +			am.StoreEvent(ctx, user.Id, user.Id, userAuth.AccountId, activity.GroupAddedToUser, meta)
     		}
     	}
     
     	for _, g := range removeOldGroups {
    -		group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, accountID, g)
    +		group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, userAuth.AccountId, g)
     		if err != nil {
    -			log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, accountID)
    +			log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, userAuth.AccountId)
     		} else {
     			meta := map[string]any{
     				"group": group.Name, "group_id": group.ID,
     				"is_service_user": user.IsServiceUser, "user_name": user.ServiceUserName,
     			}
    -			am.StoreEvent(ctx, user.Id, user.Id, accountID, activity.GroupRemovedFromUser, meta)
    +			am.StoreEvent(ctx, user.Id, user.Id, userAuth.AccountId, activity.GroupRemovedFromUser, meta)
     		}
     	}
     
     	if settings.GroupsPropagationEnabled {
    -		removedGroupAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, accountID, removeOldGroups)
    +		removedGroupAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, userAuth.AccountId, removeOldGroups)
     		if err != nil {
     			return err
     		}
     
    -		newGroupsAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, accountID, addNewGroups)
    +		newGroupsAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, userAuth.AccountId, addNewGroups)
     		if err != nil {
     			return err
     		}
     
     		if removedGroupAffectsPeers || newGroupsAffectsPeers {
    -			log.WithContext(ctx).Tracef("user %s: JWT group membership changed, updating account peers", claims.UserId)
    -			am.UpdateAccountPeers(ctx, accountID)
    +			log.WithContext(ctx).Tracef("user %s: JWT group membership changed, updating account peers", userAuth.UserId)
    +			am.UpdateAccountPeers(ctx, userAuth.AccountId)
     		}
     	}
     
    @@ -1415,24 +1340,34 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     // Existing user + Existing account + Existing Indexed Domain -> Nothing changes
     //
     // Existing user + Existing account + Existing domain reclassified Domain as private -> Nothing changes (index domain)
    -func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, error) {
    +//
    +// UserAuth IsChild -> checks that account exists
    +func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context.Context, userAuth nbcontext.UserAuth) (string, error) {
     	log.WithContext(ctx).Tracef("getting account with authorization claims. User ID: \"%s\", Account ID: \"%s\", Domain: \"%s\", Domain Category: \"%s\"",
    -		claims.UserId, claims.AccountId, claims.Domain, claims.DomainCategory)
    +		userAuth.UserId, userAuth.AccountId, userAuth.Domain, userAuth.DomainCategory)
     
    -	if claims.UserId == "" {
    +	if userAuth.UserId == "" {
     		return "", errors.New(emptyUserID)
     	}
     
    -	if claims.DomainCategory != types.PrivateCategory || !isDomainValid(claims.Domain) {
    -		return am.GetAccountIDByUserID(ctx, claims.UserId, claims.Domain)
    +	if userAuth.IsChild {
    +		exists, err := am.Store.AccountExists(ctx, store.LockingStrengthShare, userAuth.AccountId)
    +		if err != nil || !exists {
    +			return "", err
    +		}
    +		return userAuth.AccountId, nil
     	}
     
    -	if claims.AccountId != "" {
    -		return am.handlePrivateAccountWithIDFromClaim(ctx, claims)
    +	if userAuth.DomainCategory != types.PrivateCategory || !isDomainValid(userAuth.Domain) {
    +		return am.GetAccountIDByUserID(ctx, userAuth.UserId, userAuth.Domain)
    +	}
    +
    +	if userAuth.AccountId != "" {
    +		return am.handlePrivateAccountWithIDFromClaim(ctx, userAuth)
     	}
     
     	// We checked if the domain has a primary account already
    -	domainAccountID, cancel, err := am.getPrivateDomainWithGlobalLock(ctx, claims.Domain)
    +	domainAccountID, cancel, err := am.getPrivateDomainWithGlobalLock(ctx, userAuth.Domain)
     	if cancel != nil {
     		defer cancel()
     	}
    @@ -1440,14 +1375,14 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context
     		return "", err
     	}
     
    -	userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, claims.UserId)
    +	userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if handleNotFound(err) != nil {
     		log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err)
     		return "", err
     	}
     
     	if userAccountID != "" {
    -		if err = am.handleExistingUserAccount(ctx, userAccountID, domainAccountID, claims); err != nil {
    +		if err = am.handleExistingUserAccount(ctx, userAccountID, domainAccountID, userAuth); err != nil {
     			return "", err
     		}
     
    @@ -1455,10 +1390,10 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context
     	}
     
     	if domainAccountID != "" {
    -		return am.addNewUserToDomainAccount(ctx, domainAccountID, claims)
    +		return am.addNewUserToDomainAccount(ctx, domainAccountID, userAuth)
     	}
     
    -	return am.addNewPrivateAccount(ctx, domainAccountID, claims)
    +	return am.addNewPrivateAccount(ctx, domainAccountID, userAuth)
     }
     func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Context, domain string) (string, context.CancelFunc, error) {
     	domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, domain)
    @@ -1486,40 +1421,40 @@ func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Cont
     	return domainAccountID, cancel, nil
     }
     
    -func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, error) {
    -	userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, claims.UserId)
    +func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context.Context, userAuth nbcontext.UserAuth) (string, error) {
    +	userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if err != nil {
     		log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err)
     		return "", err
     	}
     
    -	if userAccountID != claims.AccountId {
    -		return "", fmt.Errorf("user %s is not part of the account id %s", claims.UserId, claims.AccountId)
    +	if userAccountID != userAuth.AccountId {
    +		return "", fmt.Errorf("user %s is not part of the account id %s", userAuth.UserId, userAuth.AccountId)
     	}
     
    -	accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, claims.AccountId)
    +	accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, userAuth.AccountId)
     	if handleNotFound(err) != nil {
     		log.WithContext(ctx).Errorf("error getting account domain and category: %v", err)
     		return "", err
     	}
     
    -	if domainIsUpToDate(accountDomain, domainCategory, claims) {
    -		return claims.AccountId, nil
    +	if domainIsUpToDate(accountDomain, domainCategory, userAuth) {
    +		return userAuth.AccountId, nil
     	}
     
     	// We checked if the domain has a primary account already
    -	domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, claims.Domain)
    +	domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, userAuth.Domain)
     	if handleNotFound(err) != nil {
     		log.WithContext(ctx).Errorf(errorGettingDomainAccIDFmt, err)
     		return "", err
     	}
     
    -	err = am.handleExistingUserAccount(ctx, claims.AccountId, domainAccountID, claims)
    +	err = am.handleExistingUserAccount(ctx, userAuth.AccountId, domainAccountID, userAuth)
     	if err != nil {
     		return "", err
     	}
     
    -	return claims.AccountId, nil
    +	return userAuth.AccountId, nil
     }
     
     func handleNotFound(err error) error {
    @@ -1534,8 +1469,8 @@ func handleNotFound(err error) error {
     	return nil
     }
     
    -func domainIsUpToDate(domain string, domainCategory string, claims jwtclaims.AuthorizationClaims) bool {
    -	return domainCategory == types.PrivateCategory || claims.DomainCategory != types.PrivateCategory || domain != claims.Domain
    +func domainIsUpToDate(domain string, domainCategory string, userAuth nbcontext.UserAuth) bool {
    +	return domainCategory == types.PrivateCategory || userAuth.DomainCategory != types.PrivateCategory || domain != userAuth.Domain
     }
     
     func (am *DefaultAccountManager) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) {
    @@ -1617,34 +1552,6 @@ func (am *DefaultAccountManager) GetDNSDomain() string {
     	return am.dnsDomain
     }
     
    -// CheckUserAccessByJWTGroups checks if the user has access, particularly in cases where the admin enabled JWT
    -// group propagation and set the list of groups with access permissions.
    -func (am *DefaultAccountManager) CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error {
    -	accountID, _, err := am.GetAccountIDFromToken(ctx, claims)
    -	if err != nil {
    -		return err
    -	}
    -
    -	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
    -	if err != nil {
    -		return err
    -	}
    -
    -	// Ensures JWT group synchronization to the management is enabled before,
    -	// filtering access based on the allowed groups.
    -	if settings != nil && settings.JWTGroupsEnabled {
    -		if allowedGroups := settings.JWTAllowGroups; len(allowedGroups) > 0 {
    -			userJWTGroups := extractJWTGroups(ctx, settings.JWTGroupsClaimName, claims)
    -
    -			if !userHasAllowedGroup(allowedGroups, userJWTGroups) {
    -				return fmt.Errorf("user does not belong to any of the allowed JWT groups")
    -			}
    -		}
    -	}
    -
    -	return nil
    -}
    -
     func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, accountID string) {
     	log.WithContext(ctx).Debugf("validated peers has been invalidated for account %s", accountID)
     	am.UpdateAccountPeers(ctx, accountID)
    @@ -1802,39 +1709,6 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty
     	return acc
     }
     
    -// extractJWTGroups extracts the group names from a JWT token's claims.
    -func extractJWTGroups(ctx context.Context, claimName string, claims jwtclaims.AuthorizationClaims) []string {
    -	userJWTGroups := make([]string, 0)
    -
    -	if claim, ok := claims.Raw[claimName]; ok {
    -		if claimGroups, ok := claim.([]interface{}); ok {
    -			for _, g := range claimGroups {
    -				if group, ok := g.(string); ok {
    -					userJWTGroups = append(userJWTGroups, group)
    -				} else {
    -					log.WithContext(ctx).Debugf("JWT claim %q contains a non-string group (type: %T): %v", claimName, g, g)
    -				}
    -			}
    -		}
    -	} else {
    -		log.WithContext(ctx).Debugf("JWT claim %q is not a string array", claimName)
    -	}
    -
    -	return userJWTGroups
    -}
    -
    -// userHasAllowedGroup checks if a user belongs to any of the allowed groups.
    -func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool {
    -	for _, userGroup := range userGroups {
    -		for _, allowedGroup := range allowedGroups {
    -			if userGroup == allowedGroup {
    -				return true
    -			}
    -		}
    -	}
    -	return false
    -}
    -
     // separateGroups separates user's auto groups into non-JWT and JWT groups.
     // Returns the list of standard auto groups and a map of JWT auto groups,
     // where the keys are the group names and the values are the group IDs.
    diff --git a/management/server/account_test.go b/management/server/account_test.go
    index 7d59544e0..f203e2066 100644
    --- a/management/server/account_test.go
    +++ b/management/server/account_test.go
    @@ -2,8 +2,6 @@ package server
     
     import (
     	"context"
    -	"crypto/sha256"
    -	b64 "encoding/base64"
     	"encoding/json"
     	"fmt"
     	"io"
    @@ -15,8 +13,6 @@ import (
     	"testing"
     	"time"
     
    -	"github.com/golang-jwt/jwt"
    -
     	"github.com/netbirdio/netbird/management/server/util"
     
     	resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
    @@ -30,7 +26,7 @@ import (
     
     	nbdns "github.com/netbirdio/netbird/dns"
     	"github.com/netbirdio/netbird/management/server/activity"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/store"
    @@ -437,7 +433,7 @@ func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) {
     }
     
     func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
    -	type initUserParams jwtclaims.AuthorizationClaims
    +	type initUserParams nbcontext.UserAuth
     
     	var (
     		publicDomain  = "public.com"
    @@ -460,7 +456,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     
     	testCases := []struct {
     		name                        string
    -		inputClaims                 jwtclaims.AuthorizationClaims
    +		inputClaims                 nbcontext.UserAuth
     		inputInitUserParams         initUserParams
     		inputUpdateAttrs            bool
     		inputUpdateClaimAccount     bool
    @@ -475,7 +471,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     	}{
     		{
     			name: "New User With Public Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         publicDomain,
     				UserId:         "pub-domain-user",
     				DomainCategory: types.PublicCategory,
    @@ -492,7 +488,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "New User With Unknown Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         unknownDomain,
     				UserId:         "unknown-domain-user",
     				DomainCategory: types.UnknownCategory,
    @@ -509,7 +505,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "New User With Private Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         privateDomain,
     				UserId:         "pvt-domain-user",
     				DomainCategory: types.PrivateCategory,
    @@ -526,7 +522,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "New Regular User With Existing Private Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         privateDomain,
     				UserId:         "new-pvt-domain-user",
     				DomainCategory: types.PrivateCategory,
    @@ -544,7 +540,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "Existing User With Existing Reclassified Private Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         defaultInitAccount.Domain,
     				UserId:         defaultInitAccount.UserId,
     				DomainCategory: types.PrivateCategory,
    @@ -561,7 +557,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "Existing Account Id With Existing Reclassified Private Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         defaultInitAccount.Domain,
     				UserId:         defaultInitAccount.UserId,
     				DomainCategory: types.PrivateCategory,
    @@ -579,7 +575,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "User With Private Category And Empty Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         "",
     				UserId:         "pvt-domain-user",
     				DomainCategory: types.PrivateCategory,
    @@ -608,7 +604,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     			require.NoError(t, err, "get init account failed")
     
     			if testCase.inputUpdateAttrs {
    -				err = manager.updateAccountDomainAttributesIfNotUpToDate(context.Background(), initAccount.Id, jwtclaims.AuthorizationClaims{UserId: testCase.inputInitUserParams.UserId, Domain: testCase.inputInitUserParams.Domain, DomainCategory: testCase.inputInitUserParams.DomainCategory}, true)
    +				err = manager.updateAccountDomainAttributesIfNotUpToDate(context.Background(), initAccount.Id, nbcontext.UserAuth{UserId: testCase.inputInitUserParams.UserId, Domain: testCase.inputInitUserParams.Domain, DomainCategory: testCase.inputInitUserParams.DomainCategory}, true)
     				require.NoError(t, err, "update init user failed")
     			}
     
    @@ -616,7 +612,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     				testCase.inputClaims.AccountId = initAccount.Id
     			}
     
    -			accountID, _, err = manager.GetAccountIDFromToken(context.Background(), testCase.inputClaims)
    +			accountID, _, err = manager.GetAccountIDFromUserAuth(context.Background(), testCase.inputClaims)
     			require.NoError(t, err, "support function failed")
     
     			account, err := manager.Store.GetAccount(context.Background(), accountID)
    @@ -635,14 +631,12 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     	}
     }
     
    -func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
    +func TestDefaultAccountManager_SyncUserJWTGroups(t *testing.T) {
     	userId := "user-id"
     	domain := "test.domain"
    -
     	_ = newAccountWithId(context.Background(), "", userId, domain)
     	manager, err := createManager(t)
     	require.NoError(t, err, "unable to create account manager")
    -
     	accountID, err := manager.GetAccountIDByUserID(context.Background(), userId, domain)
     	require.NoError(t, err, "create init user failed")
     	// as initAccount was created without account id we have to take the id after account initialization
    @@ -650,65 +644,50 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
     	// it is important to set the id as it help to avoid creating additional account with empty Id and re-pointing indices to it
     	initAccount, err := manager.Store.GetAccount(context.Background(), accountID)
     	require.NoError(t, err, "get init account failed")
    -
    -	claims := jwtclaims.AuthorizationClaims{
    +	claims := nbcontext.UserAuth{
     		AccountId:      accountID, // is empty as it is based on accountID right after initialization of initAccount
     		Domain:         domain,
     		UserId:         userId,
     		DomainCategory: "test-category",
    -		Raw:            jwt.MapClaims{"idp-groups": []interface{}{"group1", "group2"}},
    +		Groups:         []string{"group1", "group2"},
     	}
    -
     	t.Run("JWT groups disabled", func(t *testing.T) {
    -		accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims)
    -		require.NoError(t, err, "get account by token failed")
    -
    +		err := manager.SyncUserJWTGroups(context.Background(), claims)
    +		require.NoError(t, err, "synt user jwt groups failed")
     		account, err := manager.Store.GetAccount(context.Background(), accountID)
     		require.NoError(t, err, "get account failed")
    -
     		require.Len(t, account.Groups, 1, "only ALL group should exists")
     	})
    -
     	t.Run("JWT groups enabled without claim name", func(t *testing.T) {
     		initAccount.Settings.JWTGroupsEnabled = true
     		err := manager.Store.SaveAccount(context.Background(), initAccount)
     		require.NoError(t, err, "save account failed")
     		require.Len(t, manager.Store.GetAllAccounts(context.Background()), 1, "only one account should exist")
    -
    -		accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims)
    -		require.NoError(t, err, "get account by token failed")
    -
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
    +		require.NoError(t, err, "synt user jwt groups failed")
     		account, err := manager.Store.GetAccount(context.Background(), accountID)
     		require.NoError(t, err, "get account failed")
    -
     		require.Len(t, account.Groups, 1, "if group claim is not set no group added from JWT")
     	})
    -
     	t.Run("JWT groups enabled", func(t *testing.T) {
     		initAccount.Settings.JWTGroupsEnabled = true
     		initAccount.Settings.JWTGroupsClaimName = "idp-groups"
     		err := manager.Store.SaveAccount(context.Background(), initAccount)
     		require.NoError(t, err, "save account failed")
     		require.Len(t, manager.Store.GetAllAccounts(context.Background()), 1, "only one account should exist")
    -
    -		accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims)
    -		require.NoError(t, err, "get account by token failed")
    -
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
    +		require.NoError(t, err, "synt user jwt groups failed")
     		account, err := manager.Store.GetAccount(context.Background(), accountID)
     		require.NoError(t, err, "get account failed")
    -
     		require.Len(t, account.Groups, 3, "groups should be added to the account")
    -
     		groupsByNames := map[string]*types.Group{}
     		for _, g := range account.Groups {
     			groupsByNames[g.Name] = g
     		}
    -
     		g1, ok := groupsByNames["group1"]
     		require.True(t, ok, "group1 should be added to the account")
     		require.Equal(t, g1.Name, "group1", "group1 name should match")
     		require.Equal(t, g1.Issued, types.GroupIssuedJWT, "group1 issued should match")
    -
     		g2, ok := groupsByNames["group2"]
     		require.True(t, ok, "group2 should be added to the account")
     		require.Equal(t, g2.Name, "group2", "group2 name should match")
    @@ -716,88 +695,6 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
     	})
     }
     
    -func TestAccountManager_GetAccountFromPAT(t *testing.T) {
    -	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
    -	if err != nil {
    -		t.Fatalf("Error when creating store: %s", err)
    -	}
    -	t.Cleanup(cleanup)
    -	account := newAccountWithId(context.Background(), "account_id", "testuser", "")
    -
    -	token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
    -	hashedToken := sha256.Sum256([]byte(token))
    -	encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:])
    -	account.Users["someUser"] = &types.User{
    -		Id: "someUser",
    -		PATs: map[string]*types.PersonalAccessToken{
    -			"tokenId": {
    -				ID:          "tokenId",
    -				UserID:      "someUser",
    -				HashedToken: encodedHashedToken,
    -			},
    -		},
    -	}
    -	err = store.SaveAccount(context.Background(), account)
    -	if err != nil {
    -		t.Fatalf("Error when saving account: %s", err)
    -	}
    -
    -	am := DefaultAccountManager{
    -		Store: store,
    -	}
    -
    -	user, pat, _, _, err := am.GetPATInfo(context.Background(), token)
    -	if err != nil {
    -		t.Fatalf("Error when getting Account from PAT: %s", err)
    -	}
    -
    -	assert.Equal(t, "account_id", user.AccountID)
    -	assert.Equal(t, "someUser", user.Id)
    -	assert.Equal(t, account.Users["someUser"].PATs["tokenId"].ID, pat.ID)
    -}
    -
    -func TestDefaultAccountManager_MarkPATUsed(t *testing.T) {
    -	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
    -	if err != nil {
    -		t.Fatalf("Error when creating store: %s", err)
    -	}
    -	t.Cleanup(cleanup)
    -
    -	account := newAccountWithId(context.Background(), "account_id", "testuser", "")
    -
    -	token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
    -	hashedToken := sha256.Sum256([]byte(token))
    -	encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:])
    -	account.Users["someUser"] = &types.User{
    -		Id: "someUser",
    -		PATs: map[string]*types.PersonalAccessToken{
    -			"tokenId": {
    -				ID:          "tokenId",
    -				HashedToken: encodedHashedToken,
    -			},
    -		},
    -	}
    -	err = store.SaveAccount(context.Background(), account)
    -	if err != nil {
    -		t.Fatalf("Error when saving account: %s", err)
    -	}
    -
    -	am := DefaultAccountManager{
    -		Store: store,
    -	}
    -
    -	err = am.MarkPATUsed(context.Background(), "tokenId")
    -	if err != nil {
    -		t.Fatalf("Error when marking PAT used: %s", err)
    -	}
    -
    -	account, err = am.Store.GetAccount(context.Background(), "account_id")
    -	if err != nil {
    -		t.Fatalf("Error when getting account: %s", err)
    -	}
    -	assert.True(t, !account.Users["someUser"].PATs["tokenId"].GetLastUsed().IsZero())
    -}
    -
     func TestAccountManager_PrivateAccount(t *testing.T) {
     	manager, err := createManager(t)
     	if err != nil {
    @@ -962,13 +859,13 @@ func TestAccountManager_DeleteAccount(t *testing.T) {
     }
     
     func BenchmarkTest_GetAccountWithclaims(b *testing.B) {
    -	claims := jwtclaims.AuthorizationClaims{
    +	claims := nbcontext.UserAuth{
     		Domain:         "example.com",
     		UserId:         "pvt-domain-user",
     		DomainCategory: types.PrivateCategory,
     	}
     
    -	publicClaims := jwtclaims.AuthorizationClaims{
    +	publicClaims := nbcontext.UserAuth{
     		Domain:         "test.com",
     		UserId:         "public-domain-user",
     		DomainCategory: types.PublicCategory,
    @@ -2683,11 +2580,13 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	assert.NoError(t, manager.Store.SaveAccount(context.Background(), account), "unable to save account")
     
     	t.Run("skip sync for token auth type", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group3"}, "is_token": true},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{"group3"},
    +			IsPAT:     true,
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2696,11 +2595,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("empty jwt groups", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{},
     		}
    -		err := manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err := manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2709,11 +2609,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("jwt match existing api group", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group1"}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{"group1"},
     		}
    -		err := manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err := manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2729,11 +2630,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     		account.Users["user1"].AutoGroups = []string{"group1"}
     		assert.NoError(t, manager.Store.SaveUser(context.Background(), store.LockingStrengthUpdate, account.Users["user1"]))
     
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group1"}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{"group1"},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2746,11 +2648,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("add jwt group", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group1", "group2"}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{"group1", "group2"},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2759,11 +2662,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("existed group not update", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group2"}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{"group2"},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2772,11 +2676,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("add new group", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user2",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group1", "group3"}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user2",
    +			AccountId: "accountID",
    +			Groups:    []string{"group1", "group3"},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		groups, err := manager.Store.GetAccountGroups(context.Background(), store.LockingStrengthShare, "accountID")
    @@ -2789,11 +2694,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("remove all JWT groups when list is empty", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2803,11 +2709,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("remove all JWT groups when claim does not exist", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user2",
    -			Raw:    jwt.MapClaims{},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user2",
    +			AccountId: "accountID",
    +			Groups:    []string{},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user2")
    diff --git a/management/server/jwtclaims/extractor.go b/management/server/auth/jwt/extractor.go
    similarity index 51%
    rename from management/server/jwtclaims/extractor.go
    rename to management/server/auth/jwt/extractor.go
    index 18214b434..fab429125 100644
    --- a/management/server/jwtclaims/extractor.go
    +++ b/management/server/auth/jwt/extractor.go
    @@ -1,15 +1,17 @@
    -package jwtclaims
    +package jwt
     
     import (
    -	"net/http"
    +	"errors"
    +	"net/url"
     	"time"
     
     	"github.com/golang-jwt/jwt"
    +	log "github.com/sirupsen/logrus"
    +
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     )
     
     const (
    -	// TokenUserProperty key for the user property in the request context
    -	TokenUserProperty = "user"
     	// AccountIDSuffix suffix for the account id claim
     	AccountIDSuffix = "wt_account_id"
     	// DomainIDSuffix suffix for the domain id claim
    @@ -22,19 +24,16 @@ const (
     	LastLoginSuffix = "nb_last_login"
     	// Invited claim indicates that an incoming JWT is from a user that just accepted an invitation
     	Invited = "nb_invited"
    -	// IsToken claim indicates that auth type from the user is a token
    -	IsToken = "is_token"
     )
     
    -// ExtractClaims Extract function type
    -type ExtractClaims func(r *http.Request) AuthorizationClaims
    +var (
    +	errUserIDClaimEmpty = errors.New("user ID claim token value is empty")
    +)
     
     // ClaimsExtractor struct that holds the extract function
     type ClaimsExtractor struct {
     	authAudience string
     	userIDClaim  string
    -
    -	FromRequestContext ExtractClaims
     }
     
     // ClaimsExtractorOption is a function that configures the ClaimsExtractor
    @@ -54,13 +53,6 @@ func WithUserIDClaim(userIDClaim string) ClaimsExtractorOption {
     	}
     }
     
    -// WithFromRequestContext sets the function that extracts claims from the request context
    -func WithFromRequestContext(ec ExtractClaims) ClaimsExtractorOption {
    -	return func(c *ClaimsExtractor) {
    -		c.FromRequestContext = ec
    -	}
    -}
    -
     // NewClaimsExtractor returns an extractor, and if provided with a function with ExtractClaims signature,
     // then it will use that logic. Uses ExtractClaimsFromRequestContext by default
     func NewClaimsExtractor(options ...ClaimsExtractorOption) *ClaimsExtractor {
    @@ -68,49 +60,13 @@ func NewClaimsExtractor(options ...ClaimsExtractorOption) *ClaimsExtractor {
     	for _, option := range options {
     		option(ce)
     	}
    -	if ce.FromRequestContext == nil {
    -		ce.FromRequestContext = ce.fromRequestContext
    -	}
    +
     	if ce.userIDClaim == "" {
     		ce.userIDClaim = UserIDClaim
     	}
     	return ce
     }
     
    -// FromToken extracts claims from the token (after auth)
    -func (c *ClaimsExtractor) FromToken(token *jwt.Token) AuthorizationClaims {
    -	claims := token.Claims.(jwt.MapClaims)
    -	jwtClaims := AuthorizationClaims{
    -		Raw: claims,
    -	}
    -	userID, ok := claims[c.userIDClaim].(string)
    -	if !ok {
    -		return jwtClaims
    -	}
    -	jwtClaims.UserId = userID
    -	accountIDClaim, ok := claims[c.authAudience+AccountIDSuffix]
    -	if ok {
    -		jwtClaims.AccountId = accountIDClaim.(string)
    -	}
    -	domainClaim, ok := claims[c.authAudience+DomainIDSuffix]
    -	if ok {
    -		jwtClaims.Domain = domainClaim.(string)
    -	}
    -	domainCategoryClaim, ok := claims[c.authAudience+DomainCategorySuffix]
    -	if ok {
    -		jwtClaims.DomainCategory = domainCategoryClaim.(string)
    -	}
    -	LastLoginClaimString, ok := claims[c.authAudience+LastLoginSuffix]
    -	if ok {
    -		jwtClaims.LastLogin = parseTime(LastLoginClaimString.(string))
    -	}
    -	invitedBool, ok := claims[c.authAudience+Invited]
    -	if ok {
    -		jwtClaims.Invited = invitedBool.(bool)
    -	}
    -	return jwtClaims
    -}
    -
     func parseTime(timeString string) time.Time {
     	if timeString == "" {
     		return time.Time{}
    @@ -122,11 +78,67 @@ func parseTime(timeString string) time.Time {
     	return parsedTime
     }
     
    -// fromRequestContext extracts claims from the request context previously filled by the JWT token (after auth)
    -func (c *ClaimsExtractor) fromRequestContext(r *http.Request) AuthorizationClaims {
    -	if r.Context().Value(TokenUserProperty) == nil {
    -		return AuthorizationClaims{}
    +func (c ClaimsExtractor) audienceClaim(claimName string) string {
    +	url, err := url.JoinPath(c.authAudience, claimName)
    +	if err != nil {
    +		return c.authAudience + claimName // as it was previously
     	}
    -	token := r.Context().Value(TokenUserProperty).(*jwt.Token)
    -	return c.FromToken(token)
    +
    +	return url
    +}
    +
    +func (c *ClaimsExtractor) ToUserAuth(token *jwt.Token) (nbcontext.UserAuth, error) {
    +	claims := token.Claims.(jwt.MapClaims)
    +	userAuth := nbcontext.UserAuth{}
    +
    +	userID, ok := claims[c.userIDClaim].(string)
    +	if !ok {
    +		return userAuth, errUserIDClaimEmpty
    +	}
    +	userAuth.UserId = userID
    +
    +	if accountIDClaim, ok := claims[c.audienceClaim(AccountIDSuffix)]; ok {
    +		userAuth.AccountId = accountIDClaim.(string)
    +	}
    +
    +	if domainClaim, ok := claims[c.audienceClaim(DomainIDSuffix)]; ok {
    +		userAuth.Domain = domainClaim.(string)
    +	}
    +
    +	if domainCategoryClaim, ok := claims[c.audienceClaim(DomainCategorySuffix)]; ok {
    +		userAuth.DomainCategory = domainCategoryClaim.(string)
    +	}
    +
    +	if lastLoginClaimString, ok := claims[c.audienceClaim(LastLoginSuffix)]; ok {
    +		userAuth.LastLogin = parseTime(lastLoginClaimString.(string))
    +	}
    +
    +	if invitedBool, ok := claims[c.audienceClaim(Invited)]; ok {
    +		if value, ok := invitedBool.(bool); ok {
    +			userAuth.Invited = value
    +		}
    +	}
    +
    +	return userAuth, nil
    +}
    +
    +func (c *ClaimsExtractor) ToGroups(token *jwt.Token, claimName string) []string {
    +	claims := token.Claims.(jwt.MapClaims)
    +	userJWTGroups := make([]string, 0)
    +
    +	if claim, ok := claims[claimName]; ok {
    +		if claimGroups, ok := claim.([]interface{}); ok {
    +			for _, g := range claimGroups {
    +				if group, ok := g.(string); ok {
    +					userJWTGroups = append(userJWTGroups, group)
    +				} else {
    +					log.Debugf("JWT claim %q contains a non-string group (type: %T): %v", claimName, g, g)
    +				}
    +			}
    +		}
    +	} else {
    +		log.Debugf("JWT claim %q is not a string array", claimName)
    +	}
    +
    +	return userJWTGroups
     }
    diff --git a/management/server/auth/jwt/validator.go b/management/server/auth/jwt/validator.go
    new file mode 100644
    index 000000000..5b38ca786
    --- /dev/null
    +++ b/management/server/auth/jwt/validator.go
    @@ -0,0 +1,302 @@
    +package jwt
    +
    +import (
    +	"context"
    +	"crypto/ecdsa"
    +	"crypto/elliptic"
    +	"crypto/rsa"
    +	"encoding/base64"
    +	"encoding/json"
    +	"errors"
    +	"fmt"
    +	"math/big"
    +	"net/http"
    +	"net/url"
    +	"strconv"
    +	"strings"
    +	"sync"
    +	"time"
    +
    +	"github.com/golang-jwt/jwt"
    +
    +	log "github.com/sirupsen/logrus"
    +)
    +
    +// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation
    +type Jwks struct {
    +	Keys          []JSONWebKey `json:"keys"`
    +	expiresInTime time.Time
    +}
    +
    +// The supported elliptic curves types
    +const (
    +	// p256 represents a cryptographic elliptical curve type.
    +	p256 = "P-256"
    +
    +	// p384 represents a cryptographic elliptical curve type.
    +	p384 = "P-384"
    +
    +	// p521 represents a cryptographic elliptical curve type.
    +	p521 = "P-521"
    +)
    +
    +// JSONWebKey is a representation of a Jason Web Key
    +type JSONWebKey struct {
    +	Kty string   `json:"kty"`
    +	Kid string   `json:"kid"`
    +	Use string   `json:"use"`
    +	N   string   `json:"n"`
    +	E   string   `json:"e"`
    +	Crv string   `json:"crv"`
    +	X   string   `json:"x"`
    +	Y   string   `json:"y"`
    +	X5c []string `json:"x5c"`
    +}
    +
    +type Validator struct {
    +	lock                     sync.Mutex
    +	issuer                   string
    +	audienceList             []string
    +	keysLocation             string
    +	idpSignkeyRefreshEnabled bool
    +	keys                     *Jwks
    +}
    +
    +var (
    +	errKeyNotFound     = errors.New("unable to find appropriate key")
    +	errInvalidAudience = errors.New("invalid audience")
    +	errInvalidIssuer   = errors.New("invalid issuer")
    +	errTokenEmpty      = errors.New("required authorization token not found")
    +	errTokenInvalid    = errors.New("token is invalid")
    +	errTokenParsing    = errors.New("token could not be parsed")
    +)
    +
    +func NewValidator(issuer string, audienceList []string, keysLocation string, idpSignkeyRefreshEnabled bool) *Validator {
    +	keys, err := getPemKeys(keysLocation)
    +	if err != nil {
    +		log.WithField("keysLocation", keysLocation).Errorf("could not get keys from location: %s", err)
    +	}
    +
    +	return &Validator{
    +		keys:                     keys,
    +		issuer:                   issuer,
    +		audienceList:             audienceList,
    +		keysLocation:             keysLocation,
    +		idpSignkeyRefreshEnabled: idpSignkeyRefreshEnabled,
    +	}
    +}
    +
    +func (v *Validator) getKeyFunc(ctx context.Context) jwt.Keyfunc {
    +	return func(token *jwt.Token) (interface{}, error) {
    +		// Verify 'aud' claim
    +		var checkAud bool
    +		for _, audience := range v.audienceList {
    +			checkAud = token.Claims.(jwt.MapClaims).VerifyAudience(audience, false)
    +			if checkAud {
    +				break
    +			}
    +		}
    +		if !checkAud {
    +			return token, errInvalidAudience
    +		}
    +
    +		// Verify 'issuer' claim
    +		checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(v.issuer, false)
    +		if !checkIss {
    +			return token, errInvalidIssuer
    +		}
    +
    +		// If keys are rotated, verify the keys prior to token validation
    +		if v.idpSignkeyRefreshEnabled {
    +			// If the keys are invalid, retrieve new ones
    +			// @todo propose a separate go routine to regularly check these to prevent blocking when actually
    +			// validating the token
    +			if !v.keys.stillValid() {
    +				v.lock.Lock()
    +				defer v.lock.Unlock()
    +
    +				refreshedKeys, err := getPemKeys(v.keysLocation)
    +				if err != nil {
    +					log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err)
    +					refreshedKeys = v.keys
    +				}
    +
    +				log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
    +
    +				v.keys = refreshedKeys
    +			}
    +		}
    +
    +		publicKey, err := getPublicKey(token, v.keys)
    +		if err == nil {
    +			return publicKey, nil
    +		}
    +
    +		msg := fmt.Sprintf("getPublicKey error: %s", err)
    +		if errors.Is(err, errKeyNotFound) && !v.idpSignkeyRefreshEnabled {
    +			msg = fmt.Sprintf("getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service", err)
    +		}
    +
    +		log.WithContext(ctx).Error(msg)
    +
    +		return nil, err
    +	}
    +}
    +
    +// ValidateAndParse validates the token and returns the parsed token
    +func (m *Validator) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) {
    +	// If the token is empty...
    +	if token == "" {
    +		// If we get here, the required token is missing
    +		log.WithContext(ctx).Debugf("  Error: No credentials found (CredentialsOptional=false)")
    +		return nil, errTokenEmpty
    +	}
    +
    +	// Now parse the token
    +	parsedToken, err := jwt.Parse(token, m.getKeyFunc(ctx))
    +
    +	// Check if there was an error in parsing...
    +	if err != nil {
    +		err = fmt.Errorf("%w: %s", errTokenParsing, err)
    +		log.WithContext(ctx).Error(err.Error())
    +		return nil, err
    +	}
    +
    +	// Check if the parsed token is valid...
    +	if !parsedToken.Valid {
    +		log.WithContext(ctx).Debug(errTokenInvalid.Error())
    +		return nil, errTokenInvalid
    +	}
    +
    +	return parsedToken, nil
    +}
    +
    +// stillValid returns true if the JSONWebKey still valid and have enough time to be used
    +func (jwks *Jwks) stillValid() bool {
    +	return !jwks.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(jwks.expiresInTime)
    +}
    +
    +func getPemKeys(keysLocation string) (*Jwks, error) {
    +	jwks := &Jwks{}
    +
    +	url, err := url.ParseRequestURI(keysLocation)
    +	if err != nil {
    +		return jwks, err
    +	}
    +
    +	resp, err := http.Get(url.String())
    +	if err != nil {
    +		return jwks, err
    +	}
    +	defer resp.Body.Close()
    +
    +	err = json.NewDecoder(resp.Body).Decode(jwks)
    +	if err != nil {
    +		return jwks, err
    +	}
    +
    +	cacheControlHeader := resp.Header.Get("Cache-Control")
    +	expiresIn := getMaxAgeFromCacheHeader(cacheControlHeader)
    +	jwks.expiresInTime = time.Now().Add(time.Duration(expiresIn) * time.Second)
    +
    +	return jwks, nil
    +}
    +
    +func getPublicKey(token *jwt.Token, jwks *Jwks) (interface{}, error) {
    +	// todo as we load the jkws when the server is starting, we should build a JKS map with the pem cert at the boot time
    +	for k := range jwks.Keys {
    +		if token.Header["kid"] != jwks.Keys[k].Kid {
    +			continue
    +		}
    +
    +		if len(jwks.Keys[k].X5c) != 0 {
    +			cert := "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
    +			return jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
    +		}
    +
    +		if jwks.Keys[k].Kty == "RSA" {
    +			return getPublicKeyFromRSA(jwks.Keys[k])
    +		}
    +		if jwks.Keys[k].Kty == "EC" {
    +			return getPublicKeyFromECDSA(jwks.Keys[k])
    +		}
    +	}
    +
    +	return nil, errKeyNotFound
    +}
    +
    +func getPublicKeyFromECDSA(jwk JSONWebKey) (publicKey *ecdsa.PublicKey, err error) {
    +	if jwk.X == "" || jwk.Y == "" || jwk.Crv == "" {
    +		return nil, fmt.Errorf("ecdsa key incomplete")
    +	}
    +
    +	var xCoordinate []byte
    +	if xCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.X); err != nil {
    +		return nil, err
    +	}
    +
    +	var yCoordinate []byte
    +	if yCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.Y); err != nil {
    +		return nil, err
    +	}
    +
    +	publicKey = &ecdsa.PublicKey{}
    +
    +	var curve elliptic.Curve
    +	switch jwk.Crv {
    +	case p256:
    +		curve = elliptic.P256()
    +	case p384:
    +		curve = elliptic.P384()
    +	case p521:
    +		curve = elliptic.P521()
    +	}
    +
    +	publicKey.Curve = curve
    +	publicKey.X = big.NewInt(0).SetBytes(xCoordinate)
    +	publicKey.Y = big.NewInt(0).SetBytes(yCoordinate)
    +
    +	return publicKey, nil
    +}
    +
    +func getPublicKeyFromRSA(jwk JSONWebKey) (*rsa.PublicKey, error) {
    +	decodedE, err := base64.RawURLEncoding.DecodeString(jwk.E)
    +	if err != nil {
    +		return nil, err
    +	}
    +	decodedN, err := base64.RawURLEncoding.DecodeString(jwk.N)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	var n, e big.Int
    +	e.SetBytes(decodedE)
    +	n.SetBytes(decodedN)
    +
    +	return &rsa.PublicKey{
    +		E: int(e.Int64()),
    +		N: &n,
    +	}, nil
    +}
    +
    +// getMaxAgeFromCacheHeader extracts max-age directive from the Cache-Control header
    +func getMaxAgeFromCacheHeader(cacheControl string) int {
    +	// Split into individual directives
    +	directives := strings.Split(cacheControl, ",")
    +
    +	for _, directive := range directives {
    +		directive = strings.TrimSpace(directive)
    +		if strings.HasPrefix(directive, "max-age=") {
    +			// Extract the max-age value
    +			maxAgeStr := strings.TrimPrefix(directive, "max-age=")
    +			maxAge, err := strconv.Atoi(maxAgeStr)
    +			if err != nil {
    +				return 0
    +			}
    +
    +			return maxAge
    +		}
    +	}
    +
    +	return 0
    +}
    diff --git a/management/server/auth/manager.go b/management/server/auth/manager.go
    new file mode 100644
    index 000000000..6835a3ced
    --- /dev/null
    +++ b/management/server/auth/manager.go
    @@ -0,0 +1,170 @@
    +package auth
    +
    +import (
    +	"context"
    +	"crypto/sha256"
    +	"encoding/base64"
    +	"fmt"
    +	"hash/crc32"
    +
    +	"github.com/golang-jwt/jwt"
    +
    +	"github.com/netbirdio/netbird/base62"
    +	nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +	"github.com/netbirdio/netbird/management/server/store"
    +	"github.com/netbirdio/netbird/management/server/types"
    +)
    +
    +var _ Manager = (*manager)(nil)
    +
    +type Manager interface {
    +	ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error)
    +	EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error)
    +	MarkPATUsed(ctx context.Context, tokenID string) error
    +	GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
    +}
    +
    +type manager struct {
    +	store store.Store
    +
    +	validator *nbjwt.Validator
    +	extractor *nbjwt.ClaimsExtractor
    +}
    +
    +func NewManager(store store.Store, issuer, audience, keysLocation, userIdClaim string, allAudiences []string, idpRefreshKeys bool) Manager {
    +	// @note if invalid/missing parameters are sent the validator will instantiate
    +	// but it will fail when validating and parsing the token
    +	jwtValidator := nbjwt.NewValidator(
    +		issuer,
    +		allAudiences,
    +		keysLocation,
    +		idpRefreshKeys,
    +	)
    +
    +	claimsExtractor := nbjwt.NewClaimsExtractor(
    +		nbjwt.WithAudience(audience),
    +		nbjwt.WithUserIDClaim(userIdClaim),
    +	)
    +
    +	return &manager{
    +		store: store,
    +
    +		validator: jwtValidator,
    +		extractor: claimsExtractor,
    +	}
    +}
    +
    +func (m *manager) ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error) {
    +	token, err := m.validator.ValidateAndParse(ctx, value)
    +	if err != nil {
    +		return nbcontext.UserAuth{}, nil, err
    +	}
    +
    +	userAuth, err := m.extractor.ToUserAuth(token)
    +	if err != nil {
    +		return nbcontext.UserAuth{}, nil, err
    +	}
    +	return userAuth, token, err
    +}
    +
    +func (m *manager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) {
    +	if userAuth.IsChild || userAuth.IsPAT {
    +		return userAuth, nil
    +	}
    +
    +	settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthShare, userAuth.AccountId)
    +	if err != nil {
    +		return userAuth, err
    +	}
    +
    +	// Ensures JWT group synchronization to the management is enabled before,
    +	// filtering access based on the allowed groups.
    +	if settings != nil && settings.JWTGroupsEnabled {
    +		userAuth.Groups = m.extractor.ToGroups(token, settings.JWTGroupsClaimName)
    +		if allowedGroups := settings.JWTAllowGroups; len(allowedGroups) > 0 {
    +			if !userHasAllowedGroup(allowedGroups, userAuth.Groups) {
    +				return userAuth, fmt.Errorf("user does not belong to any of the allowed JWT groups")
    +			}
    +		}
    +	}
    +
    +	return userAuth, nil
    +}
    +
    +// MarkPATUsed marks a personal access token as used
    +func (am *manager) MarkPATUsed(ctx context.Context, tokenID string) error {
    +	return am.store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID)
    +}
    +
    +// GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token.
    +func (am *manager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
    +	user, pat, err = am.extractPATFromToken(ctx, token)
    +	if err != nil {
    +		return nil, nil, "", "", err
    +	}
    +
    +	domain, category, err = am.store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID)
    +	if err != nil {
    +		return nil, nil, "", "", err
    +	}
    +
    +	return user, pat, domain, category, nil
    +}
    +
    +// extractPATFromToken validates the token structure and retrieves associated User and PAT.
    +func (am *manager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) {
    +	if len(token) != types.PATLength {
    +		return nil, nil, fmt.Errorf("PAT has incorrect length")
    +	}
    +
    +	prefix := token[:len(types.PATPrefix)]
    +	if prefix != types.PATPrefix {
    +		return nil, nil, fmt.Errorf("PAT has wrong prefix")
    +	}
    +	secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength]
    +	encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength]
    +
    +	verificationChecksum, err := base62.Decode(encodedChecksum)
    +	if err != nil {
    +		return nil, nil, fmt.Errorf("PAT checksum decoding failed: %w", err)
    +	}
    +
    +	secretChecksum := crc32.ChecksumIEEE([]byte(secret))
    +	if secretChecksum != verificationChecksum {
    +		return nil, nil, fmt.Errorf("PAT checksum does not match")
    +	}
    +
    +	hashedToken := sha256.Sum256([]byte(token))
    +	encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])
    +
    +	var user *types.User
    +	var pat *types.PersonalAccessToken
    +
    +	err = am.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
    +		pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken)
    +		if err != nil {
    +			return err
    +		}
    +
    +		user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID)
    +		return err
    +	})
    +	if err != nil {
    +		return nil, nil, err
    +	}
    +
    +	return user, pat, nil
    +}
    +
    +// userHasAllowedGroup checks if a user belongs to any of the allowed groups.
    +func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool {
    +	for _, userGroup := range userGroups {
    +		for _, allowedGroup := range allowedGroups {
    +			if userGroup == allowedGroup {
    +				return true
    +			}
    +		}
    +	}
    +	return false
    +}
    diff --git a/management/server/auth/manager_mock.go b/management/server/auth/manager_mock.go
    new file mode 100644
    index 000000000..bc7066548
    --- /dev/null
    +++ b/management/server/auth/manager_mock.go
    @@ -0,0 +1,54 @@
    +package auth
    +
    +import (
    +	"context"
    +
    +	"github.com/golang-jwt/jwt"
    +
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +	"github.com/netbirdio/netbird/management/server/types"
    +)
    +
    +var (
    +	_ Manager = (*MockManager)(nil)
    +)
    +
    +// @note really dislike this mocking approach but rather than have to do additional test refactoring.
    +type MockManager struct {
    +	ValidateAndParseTokenFunc       func(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error)
    +	EnsureUserAccessByJWTGroupsFunc func(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error)
    +	MarkPATUsedFunc                 func(ctx context.Context, tokenID string) error
    +	GetPATInfoFunc                  func(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
    +}
    +
    +// EnsureUserAccessByJWTGroups implements Manager.
    +func (m *MockManager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) {
    +	if m.EnsureUserAccessByJWTGroupsFunc != nil {
    +		return m.EnsureUserAccessByJWTGroupsFunc(ctx, userAuth, token)
    +	}
    +	return nbcontext.UserAuth{}, nil
    +}
    +
    +// GetPATInfo implements Manager.
    +func (m *MockManager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
    +	if m.GetPATInfoFunc != nil {
    +		return m.GetPATInfoFunc(ctx, token)
    +	}
    +	return &types.User{}, &types.PersonalAccessToken{}, "", "", nil
    +}
    +
    +// MarkPATUsed implements Manager.
    +func (m *MockManager) MarkPATUsed(ctx context.Context, tokenID string) error {
    +	if m.MarkPATUsedFunc != nil {
    +		return m.MarkPATUsedFunc(ctx, tokenID)
    +	}
    +	return nil
    +}
    +
    +// ValidateAndParseToken implements Manager.
    +func (m *MockManager) ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error) {
    +	if m.ValidateAndParseTokenFunc != nil {
    +		return m.ValidateAndParseTokenFunc(ctx, value)
    +	}
    +	return nbcontext.UserAuth{}, &jwt.Token{}, nil
    +}
    diff --git a/management/server/auth/manager_test.go b/management/server/auth/manager_test.go
    new file mode 100644
    index 000000000..55fb1e31a
    --- /dev/null
    +++ b/management/server/auth/manager_test.go
    @@ -0,0 +1,407 @@
    +package auth_test
    +
    +import (
    +	"context"
    +	"crypto/sha256"
    +	"encoding/base64"
    +	"fmt"
    +	"net/http"
    +	"net/http/httptest"
    +	"os"
    +	"strings"
    +	"testing"
    +	"time"
    +
    +	"github.com/golang-jwt/jwt"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/netbirdio/netbird/management/server/auth"
    +	nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +	"github.com/netbirdio/netbird/management/server/store"
    +	"github.com/netbirdio/netbird/management/server/types"
    +)
    +
    +func TestAuthManager_GetAccountInfoFromPAT(t *testing.T) {
    +	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
    +	if err != nil {
    +		t.Fatalf("Error when creating store: %s", err)
    +	}
    +	t.Cleanup(cleanup)
    +
    +	token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
    +	hashedToken := sha256.Sum256([]byte(token))
    +	encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])
    +	account := &types.Account{
    +		Id: "account_id",
    +		Users: map[string]*types.User{"someUser": {
    +			Id: "someUser",
    +			PATs: map[string]*types.PersonalAccessToken{
    +				"tokenId": {
    +					ID:          "tokenId",
    +					UserID:      "someUser",
    +					HashedToken: encodedHashedToken,
    +				},
    +			},
    +		}},
    +	}
    +
    +	err = store.SaveAccount(context.Background(), account)
    +	if err != nil {
    +		t.Fatalf("Error when saving account: %s", err)
    +	}
    +
    +	manager := auth.NewManager(store, "", "", "", "", []string{}, false)
    +
    +	user, pat, _, _, err := manager.GetPATInfo(context.Background(), token)
    +	if err != nil {
    +		t.Fatalf("Error when getting Account from PAT: %s", err)
    +	}
    +
    +	assert.Equal(t, "account_id", user.AccountID)
    +	assert.Equal(t, "someUser", user.Id)
    +	assert.Equal(t, account.Users["someUser"].PATs["tokenId"].ID, pat.ID)
    +}
    +
    +func TestAuthManager_MarkPATUsed(t *testing.T) {
    +	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
    +	if err != nil {
    +		t.Fatalf("Error when creating store: %s", err)
    +	}
    +	t.Cleanup(cleanup)
    +
    +	token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
    +	hashedToken := sha256.Sum256([]byte(token))
    +	encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])
    +	account := &types.Account{
    +		Id: "account_id",
    +		Users: map[string]*types.User{"someUser": {
    +			Id: "someUser",
    +			PATs: map[string]*types.PersonalAccessToken{
    +				"tokenId": {
    +					ID:          "tokenId",
    +					HashedToken: encodedHashedToken,
    +				},
    +			},
    +		}},
    +	}
    +
    +	err = store.SaveAccount(context.Background(), account)
    +	if err != nil {
    +		t.Fatalf("Error when saving account: %s", err)
    +	}
    +
    +	manager := auth.NewManager(store, "", "", "", "", []string{}, false)
    +
    +	err = manager.MarkPATUsed(context.Background(), "tokenId")
    +	if err != nil {
    +		t.Fatalf("Error when marking PAT used: %s", err)
    +	}
    +
    +	account, err = store.GetAccount(context.Background(), "account_id")
    +	if err != nil {
    +		t.Fatalf("Error when getting account: %s", err)
    +	}
    +	assert.True(t, !account.Users["someUser"].PATs["tokenId"].GetLastUsed().IsZero())
    +}
    +
    +func TestAuthManager_EnsureUserAccessByJWTGroups(t *testing.T) {
    +	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
    +	if err != nil {
    +		t.Fatalf("Error when creating store: %s", err)
    +	}
    +	t.Cleanup(cleanup)
    +
    +	userId := "user-id"
    +	domain := "test.domain"
    +
    +	account := &types.Account{
    +		Id:     "account_id",
    +		Domain: domain,
    +		Users: map[string]*types.User{"someUser": {
    +			Id: "someUser",
    +		}},
    +		Settings: &types.Settings{},
    +	}
    +
    +	err = store.SaveAccount(context.Background(), account)
    +	if err != nil {
    +		t.Fatalf("Error when saving account: %s", err)
    +	}
    +
    +	// this has been validated and parsed by ValidateAndParseToken
    +	userAuth := nbcontext.UserAuth{
    +		AccountId:      account.Id,
    +		Domain:         domain,
    +		UserId:         userId,
    +		DomainCategory: "test-category",
    +		// Groups:         []string{"group1", "group2"},
    +	}
    +
    +	// these tests only assert groups are parsed from token as per account settings
    +	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"idp-groups": []interface{}{"group1", "group2"}})
    +
    +	manager := auth.NewManager(store, "", "", "", "", []string{}, false)
    +
    +	t.Run("JWT groups disabled", func(t *testing.T) {
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +		require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups")
    +	})
    +
    +	t.Run("User impersonated", func(t *testing.T) {
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +		require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups")
    +	})
    +
    +	t.Run("User PAT", func(t *testing.T) {
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +		require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups")
    +	})
    +
    +	t.Run("JWT groups enabled without claim name", func(t *testing.T) {
    +		account.Settings.JWTGroupsEnabled = true
    +		err := store.SaveAccount(context.Background(), account)
    +		require.NoError(t, err, "save account failed")
    +
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +		require.Len(t, userAuth.Groups, 0, "account missing groups claim name")
    +	})
    +
    +	t.Run("JWT groups enabled without allowed groups", func(t *testing.T) {
    +		account.Settings.JWTGroupsEnabled = true
    +		account.Settings.JWTGroupsClaimName = "idp-groups"
    +		err := store.SaveAccount(context.Background(), account)
    +		require.NoError(t, err, "save account failed")
    +
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +		require.Equal(t, []string{"group1", "group2"}, userAuth.Groups, "group parsed do not match")
    +	})
    +
    +	t.Run("User in allowed JWT groups", func(t *testing.T) {
    +		account.Settings.JWTGroupsEnabled = true
    +		account.Settings.JWTGroupsClaimName = "idp-groups"
    +		account.Settings.JWTAllowGroups = []string{"group1"}
    +		err := store.SaveAccount(context.Background(), account)
    +		require.NoError(t, err, "save account failed")
    +
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +
    +		require.Equal(t, []string{"group1", "group2"}, userAuth.Groups, "group parsed do not match")
    +	})
    +
    +	t.Run("User not in allowed JWT groups", func(t *testing.T) {
    +		account.Settings.JWTGroupsEnabled = true
    +		account.Settings.JWTGroupsClaimName = "idp-groups"
    +		account.Settings.JWTAllowGroups = []string{"not-a-group"}
    +		err := store.SaveAccount(context.Background(), account)
    +		require.NoError(t, err, "save account failed")
    +
    +		_, err = manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.Error(t, err, "ensure user access is not in allowed groups")
    +	})
    +}
    +
    +func TestAuthManager_ValidateAndParseToken(t *testing.T) {
    +	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		w.Header().Add("Cache-Control", "max-age=30") // set a 30s expiry to these keys
    +		http.ServeFile(w, r, "test_data/jwks.json")
    +	}))
    +	defer server.Close()
    +
    +	issuer := "http://issuer.local"
    +	audience := "http://audience.local"
    +	userIdClaim := "" // defaults to "sub"
    +
    +	// we're only testing with RSA256
    +	keyData, _ := os.ReadFile("test_data/sample_key")
    +	key, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData)
    +	keyId := "test-key"
    +
    +	// note, we can use a nil store because ValidateAndParseToken does not use it in it's flow
    +	manager := auth.NewManager(nil, issuer, audience, server.URL, userIdClaim, []string{audience}, false)
    +
    +	customClaim := func(name string) string {
    +		return fmt.Sprintf("%s/%s", audience, name)
    +	}
    +
    +	lastLogin := time.Date(2025, 2, 12, 14, 25, 26, 0, time.UTC) //"2025-02-12T14:25:26.186Z"
    +
    +	tests := []struct {
    +		name      string
    +		tokenFunc func() string
    +		expected  *nbcontext.UserAuth // nil indicates expected error
    +	}{
    +		{
    +			name: "Valid with custom claims",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss":                                   issuer,
    +					"aud":                                   []string{audience},
    +					"iat":                                   time.Now().Unix(),
    +					"exp":                                   time.Now().Add(time.Hour * 1).Unix(),
    +					"sub":                                   "user-id|123",
    +					customClaim(nbjwt.AccountIDSuffix):      "account-id|567",
    +					customClaim(nbjwt.DomainIDSuffix):       "http://localhost",
    +					customClaim(nbjwt.DomainCategorySuffix): "private",
    +					customClaim(nbjwt.LastLoginSuffix):      lastLogin.Format(time.RFC3339),
    +					customClaim(nbjwt.Invited):              false,
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +			expected: &nbcontext.UserAuth{
    +				UserId:         "user-id|123",
    +				AccountId:      "account-id|567",
    +				Domain:         "http://localhost",
    +				DomainCategory: "private",
    +				LastLogin:      lastLogin,
    +				Invited:        false,
    +			},
    +		},
    +		{
    +			name: "Valid without custom claims",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": issuer,
    +					"aud": []string{audience},
    +					"iat": time.Now().Unix(),
    +					"exp": time.Now().Add(time.Hour).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +			expected: &nbcontext.UserAuth{
    +				UserId: "user-id|123",
    +			},
    +		},
    +		{
    +			name: "Expired token",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": issuer,
    +					"aud": []string{audience},
    +					"iat": time.Now().Add(time.Hour * -2).Unix(),
    +					"exp": time.Now().Add(time.Hour * -1).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +		},
    +		{
    +			name: "Not yet valid",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": issuer,
    +					"aud": []string{audience},
    +					"iat": time.Now().Add(time.Hour).Unix(),
    +					"exp": time.Now().Add(time.Hour * 2).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +		},
    +		{
    +			name: "Invalid signature",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": issuer,
    +					"aud": []string{audience},
    +					"iat": time.Now().Unix(),
    +					"exp": time.Now().Add(time.Hour).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				parts := strings.Split(tokenString, ".")
    +				parts[2] = "invalid-signature"
    +				return strings.Join(parts, ".")
    +			},
    +		},
    +		{
    +			name: "Invalid issuer",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": "not-the-issuer",
    +					"aud": []string{audience},
    +					"iat": time.Now().Unix(),
    +					"exp": time.Now().Add(time.Hour).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +		},
    +		{
    +			name: "Invalid audience",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": issuer,
    +					"aud": []string{"not-the-audience"},
    +					"iat": time.Now().Unix(),
    +					"exp": time.Now().Add(time.Hour).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +		},
    +		{
    +			name: "Invalid user claim",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss":     issuer,
    +					"aud":     []string{audience},
    +					"iat":     time.Now().Unix(),
    +					"exp":     time.Now().Add(time.Hour).Unix(),
    +					"not-sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			tokenString := tt.tokenFunc()
    +
    +			userAuth, token, err := manager.ValidateAndParseToken(context.Background(), tokenString)
    +
    +			if tt.expected != nil {
    +				assert.NoError(t, err)
    +				assert.True(t, token.Valid)
    +				assert.Equal(t, *tt.expected, userAuth)
    +			} else {
    +				assert.Error(t, err)
    +				assert.Nil(t, token)
    +				assert.Empty(t, userAuth)
    +			}
    +		})
    +	}
    +
    +}
    diff --git a/management/server/auth/test_data/jwks.json b/management/server/auth/test_data/jwks.json
    new file mode 100644
    index 000000000..8080f5599
    --- /dev/null
    +++ b/management/server/auth/test_data/jwks.json
    @@ -0,0 +1,11 @@
    +{
    +    "keys": [
    +        {
    +            "kty": "RSA",
    +            "kid": "test-key",
    +            "use": "sig",
    +            "n": "4f5wg5l2hKsTeNem_V41fGnJm6gOdrj8ym3rFkEU_wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn_MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR-1DcKJzQBSTAGnpYVaqpsARap-nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7w",
    +            "e": "AQAB"
    +        }
    +    ]
    +}
    \ No newline at end of file
    diff --git a/management/server/auth/test_data/sample_key b/management/server/auth/test_data/sample_key
    new file mode 100644
    index 000000000..e69284a3f
    --- /dev/null
    +++ b/management/server/auth/test_data/sample_key
    @@ -0,0 +1,27 @@
    +-----BEGIN RSA PRIVATE KEY-----
    +MIIEowIBAAKCAQEA4f5wg5l2hKsTeNem/V41fGnJm6gOdrj8ym3rFkEU/wT8RDtn
    +SgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0i
    +cqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhC
    +PUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR+1DcKJzQBSTAGnpYVaqpsAR
    +ap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKA
    +Rdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7wIDAQABAoIBAQCwia1k7+2oZ2d3
    +n6agCAbqIE1QXfCmh41ZqJHbOY3oRQG3X1wpcGH4Gk+O+zDVTV2JszdcOt7E5dAy
    +MaomETAhRxB7hlIOnEN7WKm+dGNrKRvV0wDU5ReFMRHg31/Lnu8c+5BvGjZX+ky9
    +POIhFFYJqwCRlopGSUIxmVj5rSgtzk3iWOQXr+ah1bjEXvlxDOWkHN6YfpV5ThdE
    +KdBIPGEVqa63r9n2h+qazKrtiRqJqGnOrHzOECYbRFYhexsNFz7YT02xdfSHn7gM
    +IvabDDP/Qp0PjE1jdouiMaFHYnLBbgvlnZW9yuVf/rpXTUq/njxIXMmvmEyyvSDn
    +FcFikB8pAoGBAPF77hK4m3/rdGT7X8a/gwvZ2R121aBcdPwEaUhvj/36dx596zvY
    +mEOjrWfZhF083/nYWE2kVquj2wjs+otCLfifEEgXcVPTnEOPO9Zg3uNSL0nNQghj
    +FuD3iGLTUBCtM66oTe0jLSslHe8gLGEQqyMzHOzYxNqibxcOZIe8Qt0NAoGBAO+U
    +I5+XWjWEgDmvyC3TrOSf/KCGjtu0TSv30ipv27bDLMrpvPmD/5lpptTFwcxvVhCs
    +2b+chCjlghFSWFbBULBrfci2FtliClOVMYrlNBdUSJhf3aYSG2Doe6Bgt1n2CpNn
    +/iu37Y3NfemZBJA7hNl4dYe+f+uzM87cdQ214+jrAoGAXA0XxX8ll2+ToOLJsaNT
    +OvNB9h9Uc5qK5X5w+7G7O998BN2PC/MWp8H+2fVqpXgNENpNXttkRm1hk1dych86
    +EunfdPuqsX+as44oCyJGFHVBnWpm33eWQw9YqANRI+pCJzP08I5WK3osnPiwshd+
    +hR54yjgfYhBFNI7B95PmEQkCgYBzFSz7h1+s34Ycr8SvxsOBWxymG5zaCsUbPsL0
    +4aCgLScCHb9J+E86aVbbVFdglYa5Id7DPTL61ixhl7WZjujspeXZGSbmq0Kcnckb
    +mDgqkLECiOJW2NHP/j0McAkDLL4tysF8TLDO8gvuvzNC+WQ6drO2ThrypLVZQ+ry
    +eBIPmwKBgEZxhqa0gVvHQG/7Od69KWj4eJP28kq13RhKay8JOoN0vPmspXJo1HY3
    +CKuHRG+AP579dncdUnOMvfXOtkdM4vk0+hWASBQzM9xzVcztCa+koAugjVaLS9A+
    +9uQoqEeVNTckxx0S2bYevRy7hGQmUJTyQm3j1zEUR5jpdbL83Fbq
    +-----END RSA PRIVATE KEY-----
    \ No newline at end of file
    diff --git a/management/server/auth/test_data/sample_key.pub b/management/server/auth/test_data/sample_key.pub
    new file mode 100644
    index 000000000..d5b7f7102
    --- /dev/null
    +++ b/management/server/auth/test_data/sample_key.pub
    @@ -0,0 +1,9 @@
    +-----BEGIN PUBLIC KEY-----
    +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4f5wg5l2hKsTeNem/V41
    +fGnJm6gOdrj8ym3rFkEU/wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7
    +mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBp
    +HssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2
    +XrHhR+1DcKJzQBSTAGnpYVaqpsARap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3b
    +ODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy
    +7wIDAQAB
    +-----END PUBLIC KEY-----
    \ No newline at end of file
    diff --git a/management/server/config.go b/management/server/config.go
    index 397b5f0e6..ce2ff4d16 100644
    --- a/management/server/config.go
    +++ b/management/server/config.go
    @@ -2,7 +2,6 @@ package server
     
     import (
     	"net/netip"
    -	"net/url"
     
     	"github.com/netbirdio/netbird/management/server/idp"
     	"github.com/netbirdio/netbird/management/server/store"
    @@ -180,9 +179,3 @@ type ReverseProxy struct {
     	// trusted IP prefixes.
     	TrustedPeers []netip.Prefix
     }
    -
    -// validateURL validates input http url
    -func validateURL(httpURL string) bool {
    -	_, err := url.ParseRequestURI(httpURL)
    -	return err == nil
    -}
    diff --git a/management/server/context/auth.go b/management/server/context/auth.go
    new file mode 100644
    index 000000000..5cb28ddb7
    --- /dev/null
    +++ b/management/server/context/auth.go
    @@ -0,0 +1,60 @@
    +package context
    +
    +import (
    +	"context"
    +	"fmt"
    +	"net/http"
    +	"time"
    +)
    +
    +type key int
    +
    +const (
    +	UserAuthContextKey key = iota
    +)
    +
    +type UserAuth struct {
    +	// The account id the user is accessing
    +	AccountId string
    +	// The account domain
    +	Domain string
    +	// The account domain category, TBC values
    +	DomainCategory string
    +	// Indicates whether this user was invited, TBC logic
    +	Invited bool
    +	// Indicates whether this is a child account
    +	IsChild bool
    +
    +	// The user id
    +	UserId string
    +	// Last login time for this user
    +	LastLogin time.Time
    +	// The Groups the user belongs to on this account
    +	Groups []string
    +
    +	// Indicates whether this user has authenticated with a Personal Access Token
    +	IsPAT bool
    +}
    +
    +func GetUserAuthFromRequest(r *http.Request) (UserAuth, error) {
    +	return GetUserAuthFromContext(r.Context())
    +}
    +
    +func SetUserAuthInRequest(r *http.Request, userAuth UserAuth) *http.Request {
    +	return r.WithContext(SetUserAuthInContext(r.Context(), userAuth))
    +}
    +
    +func GetUserAuthFromContext(ctx context.Context) (UserAuth, error) {
    +	if userAuth, ok := ctx.Value(UserAuthContextKey).(UserAuth); ok {
    +		return userAuth, nil
    +	}
    +	return UserAuth{}, fmt.Errorf("user auth not in context")
    +}
    +
    +func SetUserAuthInContext(ctx context.Context, userAuth UserAuth) context.Context {
    +	//nolint
    +	ctx = context.WithValue(ctx, UserIDKey, userAuth.UserId)
    +	//nolint
    +	ctx = context.WithValue(ctx, AccountIDKey, userAuth.AccountId)
    +	return context.WithValue(ctx, UserAuthContextKey, userAuth)
    +}
    diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go
    index 8f5fae3e4..9d1bc1deb 100644
    --- a/management/server/grpcserver.go
    +++ b/management/server/grpcserver.go
    @@ -20,8 +20,8 @@ import (
     
     	"github.com/netbirdio/netbird/encryption"
     	"github.com/netbirdio/netbird/management/proto"
    +	"github.com/netbirdio/netbird/management/server/auth"
     	nbContext "github.com/netbirdio/netbird/management/server/context"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/settings"
    @@ -39,11 +39,10 @@ type GRPCServer struct {
     	peersUpdateManager *PeersUpdateManager
     	config             *Config
     	secretsManager     SecretsManager
    -	jwtValidator       jwtclaims.JWTValidator
    -	jwtClaimsExtractor *jwtclaims.ClaimsExtractor
     	appMetrics         telemetry.AppMetrics
     	ephemeralManager   *EphemeralManager
     	peerLocks          sync.Map
    +	authManager        auth.Manager
     }
     
     // NewServer creates a new Management server
    @@ -56,29 +55,13 @@ func NewServer(
     	secretsManager SecretsManager,
     	appMetrics telemetry.AppMetrics,
     	ephemeralManager *EphemeralManager,
    +	authManager auth.Manager,
     ) (*GRPCServer, error) {
     	key, err := wgtypes.GeneratePrivateKey()
     	if err != nil {
     		return nil, err
     	}
     
    -	var jwtValidator jwtclaims.JWTValidator
    -
    -	if config.HttpConfig != nil && config.HttpConfig.AuthIssuer != "" && config.HttpConfig.AuthAudience != "" && validateURL(config.HttpConfig.AuthKeysLocation) {
    -		jwtValidator, err = jwtclaims.NewJWTValidator(
    -			ctx,
    -			config.HttpConfig.AuthIssuer,
    -			config.GetAuthAudiences(),
    -			config.HttpConfig.AuthKeysLocation,
    -			config.HttpConfig.IdpSignKeyRefreshEnabled,
    -		)
    -		if err != nil {
    -			return nil, status.Errorf(codes.Internal, "unable to create new jwt middleware, err: %v", err)
    -		}
    -	} else {
    -		log.WithContext(ctx).Debug("unable to use http config to create new jwt middleware")
    -	}
    -
     	if appMetrics != nil {
     		// update gauge based on number of connected peers which is equal to open gRPC streams
     		err = appMetrics.GRPCMetrics().RegisterConnectedStreams(func() int64 {
    @@ -89,16 +72,6 @@ func NewServer(
     		}
     	}
     
    -	var audience, userIDClaim string
    -	if config.HttpConfig != nil {
    -		audience = config.HttpConfig.AuthAudience
    -		userIDClaim = config.HttpConfig.AuthUserIDClaim
    -	}
    -	jwtClaimsExtractor := jwtclaims.NewClaimsExtractor(
    -		jwtclaims.WithAudience(audience),
    -		jwtclaims.WithUserIDClaim(userIDClaim),
    -	)
    -
     	return &GRPCServer{
     		wgKey: key,
     		// peerKey -> event channel
    @@ -107,8 +80,7 @@ func NewServer(
     		settingsManager:    settingsManager,
     		config:             config,
     		secretsManager:     secretsManager,
    -		jwtValidator:       jwtValidator,
    -		jwtClaimsExtractor: jwtClaimsExtractor,
    +		authManager:        authManager,
     		appMetrics:         appMetrics,
     		ephemeralManager:   ephemeralManager,
     	}, nil
    @@ -294,26 +266,32 @@ func (s *GRPCServer) cancelPeerRoutines(ctx context.Context, accountID string, p
     }
     
     func (s *GRPCServer) validateToken(ctx context.Context, jwtToken string) (string, error) {
    -	if s.jwtValidator == nil {
    -		return "", status.Error(codes.Internal, "no jwt validator set")
    +	if s.authManager == nil {
    +		return "", status.Errorf(codes.Internal, "missing auth manager")
     	}
     
    -	token, err := s.jwtValidator.ValidateAndParse(ctx, jwtToken)
    +	userAuth, token, err := s.authManager.ValidateAndParseToken(ctx, jwtToken)
     	if err != nil {
     		return "", status.Errorf(codes.InvalidArgument, "invalid jwt token, err: %v", err)
     	}
    -	claims := s.jwtClaimsExtractor.FromToken(token)
    +
     	// we need to call this method because if user is new, we will automatically add it to existing or create a new account
    -	_, _, err = s.accountManager.GetAccountIDFromToken(ctx, claims)
    +	_, _, err = s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth)
     	if err != nil {
     		return "", status.Errorf(codes.Internal, "unable to fetch account with claims, err: %v", err)
     	}
     
    -	if err := s.accountManager.CheckUserAccessByJWTGroups(ctx, claims); err != nil {
    +	userAuth, err = s.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, token)
    +	if err != nil {
     		return "", status.Error(codes.PermissionDenied, err.Error())
     	}
     
    -	return claims.UserId, nil
    +	err = s.accountManager.SyncUserJWTGroups(ctx, userAuth)
    +	if err != nil {
    +		log.WithContext(ctx).Errorf("gRPC server failed to sync user JWT groups: %s", err)
    +	}
    +
    +	return userAuth.UserId, nil
     }
     
     func (s *GRPCServer) acquirePeerLockByUID(ctx context.Context, uniqueID string) (unlock func()) {
    diff --git a/management/server/http/handler.go b/management/server/http/handler.go
    index 7ce09fffa..2b87c5f25 100644
    --- a/management/server/http/handler.go
    +++ b/management/server/http/handler.go
    @@ -11,9 +11,9 @@ import (
     	"github.com/netbirdio/management-integrations/integrations"
     
     	s "github.com/netbirdio/netbird/management/server"
    +	"github.com/netbirdio/netbird/management/server/auth"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	nbgroups "github.com/netbirdio/netbird/management/server/groups"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/handlers/accounts"
     	"github.com/netbirdio/netbird/management/server/http/handlers/dns"
     	"github.com/netbirdio/netbird/management/server/http/handlers/events"
    @@ -26,7 +26,6 @@ import (
     	"github.com/netbirdio/netbird/management/server/http/handlers/users"
     	"github.com/netbirdio/netbird/management/server/http/middleware"
     	"github.com/netbirdio/netbird/management/server/integrated_validator"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbnetworks "github.com/netbirdio/netbird/management/server/networks"
     	"github.com/netbirdio/netbird/management/server/networks/resources"
     	"github.com/netbirdio/netbird/management/server/networks/routers"
    @@ -36,55 +35,51 @@ import (
     const apiPrefix = "/api"
     
     // NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints.
    -func NewAPIHandler(ctx context.Context, accountManager s.AccountManager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, jwtValidator jwtclaims.JWTValidator, appMetrics telemetry.AppMetrics, authCfg configs.AuthCfg, integratedValidator integrated_validator.IntegratedValidator) (http.Handler, error) {
    -	claimsExtractor := jwtclaims.NewClaimsExtractor(
    -		jwtclaims.WithAudience(authCfg.Audience),
    -		jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -	)
    +func NewAPIHandler(
    +	ctx context.Context,
    +	accountManager s.AccountManager,
    +	networksManager nbnetworks.Manager,
    +	resourceManager resources.Manager,
    +	routerManager routers.Manager,
    +	groupsManager nbgroups.Manager,
    +	LocationManager geolocation.Geolocation,
    +	authManager auth.Manager,
    +	appMetrics telemetry.AppMetrics,
    +	config *s.Config,
    +	integratedValidator integrated_validator.IntegratedValidator) (http.Handler, error) {
     
     	authMiddleware := middleware.NewAuthMiddleware(
    -		accountManager.GetPATInfo,
    -		jwtValidator.ValidateAndParse,
    -		accountManager.MarkPATUsed,
    -		accountManager.CheckUserAccessByJWTGroups,
    -		claimsExtractor,
    -		authCfg.Audience,
    -		authCfg.UserIDClaim,
    +		authManager,
    +		accountManager.GetAccountIDFromUserAuth,
    +		accountManager.SyncUserJWTGroups,
     	)
     
     	corsMiddleware := cors.AllowAll()
     
    -	claimsExtractor = jwtclaims.NewClaimsExtractor(
    -		jwtclaims.WithAudience(authCfg.Audience),
    -		jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -	)
    -
    -	acMiddleware := middleware.NewAccessControl(
    -		authCfg.Audience,
    -		authCfg.UserIDClaim,
    -		accountManager.GetUser)
    +	acMiddleware := middleware.NewAccessControl(accountManager.GetUserFromUserAuth)
     
     	rootRouter := mux.NewRouter()
     	metricsMiddleware := appMetrics.HTTPMiddleware()
     
     	prefix := apiPrefix
     	router := rootRouter.PathPrefix(prefix).Subrouter()
    +
     	router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler, acMiddleware.Handler)
     
    -	if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, claimsExtractor, integratedValidator, appMetrics.GetMeter()); err != nil {
    +	if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, integratedValidator, appMetrics.GetMeter()); err != nil {
     		return nil, fmt.Errorf("register integrations endpoints: %w", err)
     	}
     
    -	accounts.AddEndpoints(accountManager, authCfg, router)
    -	peers.AddEndpoints(accountManager, authCfg, router)
    -	users.AddEndpoints(accountManager, authCfg, router)
    -	setup_keys.AddEndpoints(accountManager, authCfg, router)
    -	policies.AddEndpoints(accountManager, LocationManager, authCfg, router)
    -	groups.AddEndpoints(accountManager, authCfg, router)
    -	routes.AddEndpoints(accountManager, authCfg, router)
    -	dns.AddEndpoints(accountManager, authCfg, router)
    -	events.AddEndpoints(accountManager, authCfg, router)
    -	networks.AddEndpoints(networksManager, resourceManager, routerManager, groupsManager, accountManager, accountManager.GetAccountIDFromToken, authCfg, router)
    +	accounts.AddEndpoints(accountManager, router)
    +	peers.AddEndpoints(accountManager, router)
    +	users.AddEndpoints(accountManager, router)
    +	setup_keys.AddEndpoints(accountManager, router)
    +	policies.AddEndpoints(accountManager, LocationManager, router)
    +	groups.AddEndpoints(accountManager, router)
    +	routes.AddEndpoints(accountManager, router)
    +	dns.AddEndpoints(accountManager, router)
    +	events.AddEndpoints(accountManager, router)
    +	networks.AddEndpoints(networksManager, resourceManager, routerManager, groupsManager, accountManager, router)
     
     	return rootRouter, nil
     }
    diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go
    index a23628cdc..bc0054a7f 100644
    --- a/management/server/http/handlers/accounts/accounts_handler.go
    +++ b/management/server/http/handlers/accounts/accounts_handler.go
    @@ -9,47 +9,42 @@ import (
     
     	"github.com/netbirdio/netbird/management/server"
     	"github.com/netbirdio/netbird/management/server/account"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // handler is a handler that handles the server.Account HTTP endpoints
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	accountsHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	accountsHandler := newHandler(accountManager)
     	router.HandleFunc("/accounts/{accountId}", accountsHandler.updateAccount).Methods("PUT", "OPTIONS")
     	router.HandleFunc("/accounts/{accountId}", accountsHandler.deleteAccount).Methods("DELETE", "OPTIONS")
     	router.HandleFunc("/accounts", accountsHandler.getAllAccounts).Methods("GET", "OPTIONS")
     }
     
     // newHandler creates a new handler HTTP handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllAccounts is HTTP GET handler that returns a list of accounts. Effectively returns just a single account.
     func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	settings, err := h.accountManager.GetAccountSettings(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -62,13 +57,14 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
     
     // updateAccount is HTTP PUT handler that updates the provided account. Updates only account settings (server.Settings)
     func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	_, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	_, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	accountID := vars["accountId"]
     	if len(accountID) == 0 {
    @@ -125,7 +121,12 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
     
     // deleteAccount is a HTTP DELETE handler to delete an account
     func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
    +	if err != nil {
    +		util.WriteError(r.Context(), err, w)
    +		return
    +	}
    +
     	vars := mux.Vars(r)
     	targetAccountID := vars["accountId"]
     	if len(targetAccountID) == 0 {
    @@ -133,7 +134,7 @@ func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	err := h.accountManager.DeleteAccount(r.Context(), targetAccountID, claims.UserId)
    +	err = h.accountManager.DeleteAccount(r.Context(), targetAccountID, userAuth.UserId)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
    diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go
    index e8a599863..a8d57a13f 100644
    --- a/management/server/http/handlers/accounts/accounts_handler_test.go
    +++ b/management/server/http/handlers/accounts/accounts_handler_test.go
    @@ -13,19 +13,16 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
    -func initAccountsTestData(account *types.Account, admin *types.User) *handler {
    +func initAccountsTestData(account *types.Account) *handler {
     	return &handler{
     		accountManager: &mock_server.MockAccountManager{
    -			GetAccountIDFromTokenFunc: func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return account.Id, admin.Id, nil
    -			},
     			GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
     				return account.Settings, nil
     			},
    @@ -44,15 +41,6 @@ func initAccountsTestData(account *types.Account, admin *types.User) *handler {
     				return accCopy, nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_account",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -75,7 +63,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
     			PeerLoginExpiration:        time.Hour,
     			RegularUsersViewBlocked:    true,
     		},
    -	}, adminUser)
    +	})
     
     	tt := []struct {
     		name             string
    @@ -191,6 +179,11 @@ func TestAccounts_AccountsHandler(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    adminUser.Id,
    +				AccountId: accountID,
    +				Domain:    "hotmail.com",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/accounts", handler.getAllAccounts).Methods("GET")
    diff --git a/management/server/http/handlers/dns/dns_settings_handler.go b/management/server/http/handlers/dns/dns_settings_handler.go
    index 112eee179..6ff938369 100644
    --- a/management/server/http/handlers/dns/dns_settings_handler.go
    +++ b/management/server/http/handlers/dns/dns_settings_handler.go
    @@ -8,51 +8,44 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // dnsSettingsHandler is a handler that returns the DNS settings of the account
     type dnsSettingsHandler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	addDNSSettingEndpoint(accountManager, authCfg, router)
    -	addDNSNameserversEndpoint(accountManager, authCfg, router)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	addDNSSettingEndpoint(accountManager, router)
    +	addDNSNameserversEndpoint(accountManager, router)
     }
     
    -func addDNSSettingEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	dnsSettingsHandler := newDNSSettingsHandler(accountManager, authCfg)
    +func addDNSSettingEndpoint(accountManager server.AccountManager, router *mux.Router) {
    +	dnsSettingsHandler := newDNSSettingsHandler(accountManager)
     	router.HandleFunc("/dns/settings", dnsSettingsHandler.getDNSSettings).Methods("GET", "OPTIONS")
     	router.HandleFunc("/dns/settings", dnsSettingsHandler.updateDNSSettings).Methods("PUT", "OPTIONS")
     }
     
     // newDNSSettingsHandler returns a new instance of dnsSettingsHandler handler
    -func newDNSSettingsHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *dnsSettingsHandler {
    -	return &dnsSettingsHandler{
    -		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    -	}
    +func newDNSSettingsHandler(accountManager server.AccountManager) *dnsSettingsHandler {
    +	return &dnsSettingsHandler{accountManager: accountManager}
     }
     
     // getDNSSettings returns the DNS settings for the account
     func (h *dnsSettingsHandler) getDNSSettings(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		log.WithContext(r.Context()).Error(err)
     		http.Redirect(w, r, "/", http.StatusInternalServerError)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	dnsSettings, err := h.accountManager.GetDNSSettings(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -68,13 +61,14 @@ func (h *dnsSettingsHandler) getDNSSettings(w http.ResponseWriter, r *http.Reque
     
     // updateDNSSettings handles update to DNS settings of an account
     func (h *dnsSettingsHandler) updateDNSSettings(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.PutApiDnsSettingsJSONRequestBody
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    diff --git a/management/server/http/handlers/dns/dns_settings_handler_test.go b/management/server/http/handlers/dns/dns_settings_handler_test.go
    index 9ca1dc032..ca81adf43 100644
    --- a/management/server/http/handlers/dns/dns_settings_handler_test.go
    +++ b/management/server/http/handlers/dns/dns_settings_handler_test.go
    @@ -17,7 +17,8 @@ import (
     
     	"github.com/gorilla/mux"
     
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +
     	"github.com/netbirdio/netbird/management/server/mock_server"
     )
     
    @@ -52,19 +53,7 @@ func initDNSSettingsTestData() *dnsSettingsHandler {
     				}
     				return status.Errorf(status.InvalidArgument, "the dns settings provided are nil")
     			},
    -			GetAccountIDFromTokenFunc: func(ctx context.Context, _ jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return testingDNSSettingsAccount.Id, testingDNSSettingsAccount.Users[testDNSSettingsUserID].Id, nil
    -			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: testDNSSettingsAccountID,
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -118,6 +107,11 @@ func TestDNSSettingsHandlers(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    testingDNSSettingsAccount.Users[testDNSSettingsUserID].Id,
    +				AccountId: testingDNSSettingsAccount.Id,
    +				Domain:    testingDNSSettingsAccount.Domain,
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/dns/settings", p.getDNSSettings).Methods("GET")
    diff --git a/management/server/http/handlers/dns/nameservers_handler.go b/management/server/http/handlers/dns/nameservers_handler.go
    index 09047e231..33d070477 100644
    --- a/management/server/http/handlers/dns/nameservers_handler.go
    +++ b/management/server/http/handlers/dns/nameservers_handler.go
    @@ -10,21 +10,19 @@ import (
     
     	nbdns "github.com/netbirdio/netbird/dns"
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     )
     
     // nameserversHandler is the nameserver group handler of the account
     type nameserversHandler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func addDNSNameserversEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	nameserversHandler := newNameserversHandler(accountManager, authCfg)
    +func addDNSNameserversEndpoint(accountManager server.AccountManager, router *mux.Router) {
    +	nameserversHandler := newNameserversHandler(accountManager)
     	router.HandleFunc("/dns/nameservers", nameserversHandler.getAllNameservers).Methods("GET", "OPTIONS")
     	router.HandleFunc("/dns/nameservers", nameserversHandler.createNameserverGroup).Methods("POST", "OPTIONS")
     	router.HandleFunc("/dns/nameservers/{nsgroupId}", nameserversHandler.updateNameserverGroup).Methods("PUT", "OPTIONS")
    @@ -33,26 +31,21 @@ func addDNSNameserversEndpoint(accountManager server.AccountManager, authCfg con
     }
     
     // newNameserversHandler returns a new instance of nameserversHandler handler
    -func newNameserversHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *nameserversHandler {
    -	return &nameserversHandler{
    -		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    -	}
    +func newNameserversHandler(accountManager server.AccountManager) *nameserversHandler {
    +	return &nameserversHandler{accountManager: accountManager}
     }
     
     // getAllNameservers returns the list of nameserver groups for the account
     func (h *nameserversHandler) getAllNameservers(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		log.WithContext(r.Context()).Error(err)
     		http.Redirect(w, r, "/", http.StatusInternalServerError)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	nsGroups, err := h.accountManager.ListNameServerGroups(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -69,13 +62,14 @@ func (h *nameserversHandler) getAllNameservers(w http.ResponseWriter, r *http.Re
     
     // createNameserverGroup handles nameserver group creation request
     func (h *nameserversHandler) createNameserverGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.PostApiDnsNameserversJSONRequestBody
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -102,13 +96,14 @@ func (h *nameserversHandler) createNameserverGroup(w http.ResponseWriter, r *htt
     
     // updateNameserverGroup handles update to a nameserver group identified by a given ID
     func (h *nameserversHandler) updateNameserverGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	nsGroupID := mux.Vars(r)["nsgroupId"]
     	if len(nsGroupID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w)
    @@ -153,13 +148,14 @@ func (h *nameserversHandler) updateNameserverGroup(w http.ResponseWriter, r *htt
     
     // deleteNameserverGroup handles nameserver group deletion request
     func (h *nameserversHandler) deleteNameserverGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	nsGroupID := mux.Vars(r)["nsgroupId"]
     	if len(nsGroupID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w)
    @@ -177,14 +173,14 @@ func (h *nameserversHandler) deleteNameserverGroup(w http.ResponseWriter, r *htt
     
     // getNameserverGroup handles a nameserver group Get request identified by ID
     func (h *nameserversHandler) getNameserverGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
    -		log.WithContext(r.Context()).Error(err)
    -		http.Redirect(w, r, "/", http.StatusInternalServerError)
    +		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	nsGroupID := mux.Vars(r)["nsgroupId"]
     	if len(nsGroupID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w)
    diff --git a/management/server/http/handlers/dns/nameservers_handler_test.go b/management/server/http/handlers/dns/nameservers_handler_test.go
    index c6561e4d8..45283bc37 100644
    --- a/management/server/http/handlers/dns/nameservers_handler_test.go
    +++ b/management/server/http/handlers/dns/nameservers_handler_test.go
    @@ -18,7 +18,8 @@ import (
     
     	"github.com/gorilla/mux"
     
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +
     	"github.com/netbirdio/netbird/management/server/mock_server"
     )
     
    @@ -81,19 +82,7 @@ func initNameserversTestData() *nameserversHandler {
     				}
     				return status.Errorf(status.NotFound, "nameserver group with ID %s was not found", nsGroupToSave.ID)
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: testNSGroupAccountID,
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -204,6 +193,11 @@ func TestNameserversHandlers(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				AccountId: testNSGroupAccountID,
    +				Domain:    "hotmail.com",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/dns/nameservers/{nsgroupId}", p.getNameserverGroup).Methods("GET")
    diff --git a/management/server/http/handlers/events/events_handler.go b/management/server/http/handlers/events/events_handler.go
    index 62da59535..0fb2295a8 100644
    --- a/management/server/http/handlers/events/events_handler.go
    +++ b/management/server/http/handlers/events/events_handler.go
    @@ -10,44 +10,37 @@ import (
     
     	"github.com/netbirdio/netbird/management/server"
     	"github.com/netbirdio/netbird/management/server/activity"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     )
     
     // handler HTTP handler
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	eventsHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	eventsHandler := newHandler(accountManager)
     	router.HandleFunc("/events", eventsHandler.getAllEvents).Methods("GET", "OPTIONS")
     }
     
     // newHandler creates a new events handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    -	return &handler{
    -		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    -	}
    +func newHandler(accountManager server.AccountManager) *handler {
    +	return &handler{accountManager: accountManager}
     }
     
     // getAllEvents list of the given account
     func (h *handler) getAllEvents(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		log.WithContext(r.Context()).Error(err)
     		http.Redirect(w, r, "/", http.StatusInternalServerError)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	accountEvents, err := h.accountManager.GetEvents(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    diff --git a/management/server/http/handlers/events/events_handler_test.go b/management/server/http/handlers/events/events_handler_test.go
    index fd603f289..3a643fe90 100644
    --- a/management/server/http/handlers/events/events_handler_test.go
    +++ b/management/server/http/handlers/events/events_handler_test.go
    @@ -13,9 +13,10 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +
     	"github.com/netbirdio/netbird/management/server/activity"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/types"
     )
    @@ -29,22 +30,10 @@ func initEventsTestData(account string, events ...*activity.Event) *handler {
     				}
     				return []*activity.Event{}, nil
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) {
     				return make(map[string]*types.UserInfo), nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_account",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -199,6 +188,11 @@ func TestEvents_GetEvents(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_account",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/events/", handler.getAllEvents).Methods("GET")
    diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go
    index ec635a358..040c08b87 100644
    --- a/management/server/http/handlers/groups/groups_handler.go
    +++ b/management/server/http/handlers/groups/groups_handler.go
    @@ -7,24 +7,23 @@ import (
     	"github.com/gorilla/mux"
     	log "github.com/sirupsen/logrus"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +	nbpeer "github.com/netbirdio/netbird/management/server/peer"
    +
     	"github.com/netbirdio/netbird/management/server"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
    -	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // handler is a handler that returns groups of the account
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	groupsHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	groupsHandler := newHandler(accountManager)
     	router.HandleFunc("/groups", groupsHandler.getAllGroups).Methods("GET", "OPTIONS")
     	router.HandleFunc("/groups", groupsHandler.createGroup).Methods("POST", "OPTIONS")
     	router.HandleFunc("/groups/{groupId}", groupsHandler.updateGroup).Methods("PUT", "OPTIONS")
    @@ -33,25 +32,21 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
     }
     
     // newHandler creates a new groups handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllGroups list for the account
     func (h *handler) getAllGroups(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		log.WithContext(r.Context()).Error(err)
     		http.Redirect(w, r, "/", http.StatusInternalServerError)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	groups, err := h.accountManager.GetAllGroups(r.Context(), accountID, userID)
     	if err != nil {
    @@ -75,13 +70,14 @@ func (h *handler) getAllGroups(w http.ResponseWriter, r *http.Request) {
     
     // updateGroup handles update to a group identified by a given ID
     func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	groupID, ok := vars["groupId"]
     	if !ok {
    @@ -164,13 +160,14 @@ func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) {
     
     // createGroup handles group creation request
     func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.PostApiGroupsJSONRequestBody
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -223,13 +220,14 @@ func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) {
     
     // deleteGroup handles group deletion request
     func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	groupID := mux.Vars(r)["groupId"]
     	if len(groupID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid group ID"), w)
    @@ -253,12 +251,13 @@ func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) {
     
     // getGroup returns a group
     func (h *handler) getGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	groupID := mux.Vars(r)["groupId"]
     	if len(groupID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid group ID"), w)
    diff --git a/management/server/http/handlers/groups/groups_handler_test.go b/management/server/http/handlers/groups/groups_handler_test.go
    index 0668982f3..c4b9e46ab 100644
    --- a/management/server/http/handlers/groups/groups_handler_test.go
    +++ b/management/server/http/handlers/groups/groups_handler_test.go
    @@ -18,9 +18,9 @@ import (
     	"golang.org/x/exp/maps"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
    @@ -59,9 +59,6 @@ func initGroupTestData(initGroups ...*types.Group) *handler {
     
     				return group, nil
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			GetGroupByNameFunc: func(ctx context.Context, groupName, _ string) (*types.Group, error) {
     				if groupName == "All" {
     					return &types.Group{ID: "id-all", Name: "All", Issued: types.GroupIssuedAPI}, nil
    @@ -87,15 +84,6 @@ func initGroupTestData(initGroups ...*types.Group) *handler {
     				return nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_id",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -134,6 +122,11 @@ func TestGetGroup(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/groups/{groupId}", p.getGroup).Methods("GET")
    @@ -255,6 +248,11 @@ func TestWriteGroup(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/groups", p.createGroup).Methods("POST")
    @@ -332,7 +330,11 @@ func TestDeleteGroup(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
    -
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     			router := mux.NewRouter()
     			router.HandleFunc("/api/groups/{groupId}", p.deleteGroup).Methods("DELETE")
     			router.ServeHTTP(recorder, req)
    diff --git a/management/server/http/handlers/networks/handler.go b/management/server/http/handlers/networks/handler.go
    index f716348d6..bb6b97267 100644
    --- a/management/server/http/handlers/networks/handler.go
    +++ b/management/server/http/handlers/networks/handler.go
    @@ -10,11 +10,10 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	s "github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/groups"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/networks"
     	"github.com/netbirdio/netbird/management/server/networks/resources"
     	"github.com/netbirdio/netbird/management/server/networks/routers"
    @@ -31,16 +30,14 @@ type handler struct {
     	routerManager   routers.Manager
     	accountManager  s.AccountManager
     
    -	groupsManager    groups.Manager
    -	extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
    -	claimsExtractor  *jwtclaims.ClaimsExtractor
    +	groupsManager groups.Manager
     }
     
    -func AddEndpoints(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) {
    -	addRouterEndpoints(routerManager, extractFromToken, authCfg, router)
    -	addResourceEndpoints(resourceManager, groupsManager, extractFromToken, authCfg, router)
    +func AddEndpoints(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, router *mux.Router) {
    +	addRouterEndpoints(routerManager, router)
    +	addResourceEndpoints(resourceManager, groupsManager, router)
     
    -	networksHandler := newHandler(networksManager, resourceManager, routerManager, groupsManager, accountManager, extractFromToken, authCfg)
    +	networksHandler := newHandler(networksManager, resourceManager, routerManager, groupsManager, accountManager)
     	router.HandleFunc("/networks", networksHandler.getAllNetworks).Methods("GET", "OPTIONS")
     	router.HandleFunc("/networks", networksHandler.createNetwork).Methods("POST", "OPTIONS")
     	router.HandleFunc("/networks/{networkId}", networksHandler.getNetwork).Methods("GET", "OPTIONS")
    @@ -48,29 +45,25 @@ func AddEndpoints(networksManager networks.Manager, resourceManager resources.Ma
     	router.HandleFunc("/networks/{networkId}", networksHandler.deleteNetwork).Methods("DELETE", "OPTIONS")
     }
     
    -func newHandler(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *handler {
    +func newHandler(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager) *handler {
     	return &handler{
    -		networksManager:  networksManager,
    -		resourceManager:  resourceManager,
    -		routerManager:    routerManager,
    -		groupsManager:    groupsManager,
    -		accountManager:   accountManager,
    -		extractFromToken: extractFromToken,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    +		networksManager: networksManager,
    +		resourceManager: resourceManager,
    +		routerManager:   routerManager,
    +		groupsManager:   groupsManager,
    +		accountManager:  accountManager,
     	}
     }
     
     func (h *handler) getAllNetworks(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	networks, err := h.networksManager.GetAllNetworks(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -105,12 +98,12 @@ func (h *handler) getAllNetworks(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *handler) createNetwork(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	var req api.NetworkRequest
     	err = json.NewDecoder(r.Body).Decode(&req)
    @@ -141,12 +134,12 @@ func (h *handler) createNetwork(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *handler) getNetwork(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	vars := mux.Vars(r)
     	networkID := vars["networkId"]
    @@ -179,13 +172,13 @@ func (h *handler) getNetwork(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *handler) updateNetwork(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	networkID := vars["networkId"]
     	if len(networkID) == 0 {
    @@ -229,13 +222,13 @@ func (h *handler) updateNetwork(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *handler) deleteNetwork(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	networkID := vars["networkId"]
     	if len(networkID) == 0 {
    diff --git a/management/server/http/handlers/networks/resources_handler.go b/management/server/http/handlers/networks/resources_handler.go
    index f2dc8e3b8..fba7026e8 100644
    --- a/management/server/http/handlers/networks/resources_handler.go
    +++ b/management/server/http/handlers/networks/resources_handler.go
    @@ -1,30 +1,26 @@
     package networks
     
     import (
    -	"context"
     	"encoding/json"
     	"net/http"
     
     	"github.com/gorilla/mux"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/groups"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/networks/resources"
     	"github.com/netbirdio/netbird/management/server/networks/resources/types"
     )
     
     type resourceHandler struct {
    -	resourceManager  resources.Manager
    -	groupsManager    groups.Manager
    -	extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
    -	claimsExtractor  *jwtclaims.ClaimsExtractor
    +	resourceManager resources.Manager
    +	groupsManager   groups.Manager
     }
     
    -func addResourceEndpoints(resourcesManager resources.Manager, groupsManager groups.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) {
    -	resourceHandler := newResourceHandler(resourcesManager, groupsManager, extractFromToken, authCfg)
    +func addResourceEndpoints(resourcesManager resources.Manager, groupsManager groups.Manager, router *mux.Router) {
    +	resourceHandler := newResourceHandler(resourcesManager, groupsManager)
     	router.HandleFunc("/networks/resources", resourceHandler.getAllResourcesInAccount).Methods("GET", "OPTIONS")
     	router.HandleFunc("/networks/{networkId}/resources", resourceHandler.getAllResourcesInNetwork).Methods("GET", "OPTIONS")
     	router.HandleFunc("/networks/{networkId}/resources", resourceHandler.createResource).Methods("POST", "OPTIONS")
    @@ -33,26 +29,21 @@ func addResourceEndpoints(resourcesManager resources.Manager, groupsManager grou
     	router.HandleFunc("/networks/{networkId}/resources/{resourceId}", resourceHandler.deleteResource).Methods("DELETE", "OPTIONS")
     }
     
    -func newResourceHandler(resourceManager resources.Manager, groupsManager groups.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *resourceHandler {
    +func newResourceHandler(resourceManager resources.Manager, groupsManager groups.Manager) *resourceHandler {
     	return &resourceHandler{
    -		resourceManager:  resourceManager,
    -		groupsManager:    groupsManager,
    -		extractFromToken: extractFromToken,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    +		resourceManager: resourceManager,
    +		groupsManager:   groupsManager,
     	}
     }
     
     func (h *resourceHandler) getAllResourcesInNetwork(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	networkID := mux.Vars(r)["networkId"]
     	resources, err := h.resourceManager.GetAllResourcesInNetwork(r.Context(), accountID, userID, networkID)
     	if err != nil {
    @@ -76,13 +67,14 @@ func (h *resourceHandler) getAllResourcesInNetwork(w http.ResponseWriter, r *htt
     	util.WriteJSONObject(r.Context(), w, resourcesResponse)
     }
     func (h *resourceHandler) getAllResourcesInAccount(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	resources, err := h.resourceManager.GetAllResourcesInAccount(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -106,13 +98,14 @@ func (h *resourceHandler) getAllResourcesInAccount(w http.ResponseWriter, r *htt
     }
     
     func (h *resourceHandler) createResource(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.NetworkResourceRequest
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -144,13 +137,13 @@ func (h *resourceHandler) createResource(w http.ResponseWriter, r *http.Request)
     }
     
     func (h *resourceHandler) getResource(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	networkID := mux.Vars(r)["networkId"]
     	resourceID := mux.Vars(r)["resourceId"]
     	resource, err := h.resourceManager.GetResource(r.Context(), accountID, userID, networkID, resourceID)
    @@ -171,13 +164,13 @@ func (h *resourceHandler) getResource(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *resourceHandler) updateResource(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	var req api.NetworkResourceRequest
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -209,12 +202,12 @@ func (h *resourceHandler) updateResource(w http.ResponseWriter, r *http.Request)
     }
     
     func (h *resourceHandler) deleteResource(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	networkID := mux.Vars(r)["networkId"]
     	resourceID := mux.Vars(r)["resourceId"]
    diff --git a/management/server/http/handlers/networks/routers_handler.go b/management/server/http/handlers/networks/routers_handler.go
    index 7ca95d902..f98da4966 100644
    --- a/management/server/http/handlers/networks/routers_handler.go
    +++ b/management/server/http/handlers/networks/routers_handler.go
    @@ -1,28 +1,24 @@
     package networks
     
     import (
    -	"context"
     	"encoding/json"
     	"net/http"
     
     	"github.com/gorilla/mux"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/networks/routers"
     	"github.com/netbirdio/netbird/management/server/networks/routers/types"
     )
     
     type routersHandler struct {
    -	routersManager   routers.Manager
    -	extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
    -	claimsExtractor  *jwtclaims.ClaimsExtractor
    +	routersManager routers.Manager
     }
     
    -func addRouterEndpoints(routersManager routers.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) {
    -	routersHandler := newRoutersHandler(routersManager, extractFromToken, authCfg)
    +func addRouterEndpoints(routersManager routers.Manager, router *mux.Router) {
    +	routersHandler := newRoutersHandler(routersManager)
     	router.HandleFunc("/networks/{networkId}/routers", routersHandler.getAllRouters).Methods("GET", "OPTIONS")
     	router.HandleFunc("/networks/{networkId}/routers", routersHandler.createRouter).Methods("POST", "OPTIONS")
     	router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.getRouter).Methods("GET", "OPTIONS")
    @@ -30,25 +26,21 @@ func addRouterEndpoints(routersManager routers.Manager, extractFromToken func(ct
     	router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.deleteRouter).Methods("DELETE", "OPTIONS")
     }
     
    -func newRoutersHandler(routersManager routers.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *routersHandler {
    +func newRoutersHandler(routersManager routers.Manager) *routersHandler {
     	return &routersHandler{
    -		routersManager:   routersManager,
    -		extractFromToken: extractFromToken,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    +		routersManager: routersManager,
     	}
     }
     
     func (h *routersHandler) getAllRouters(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	networkID := mux.Vars(r)["networkId"]
     	routers, err := h.routersManager.GetAllRoutersInNetwork(r.Context(), accountID, userID, networkID)
     	if err != nil {
    @@ -65,13 +57,14 @@ func (h *routersHandler) getAllRouters(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *routersHandler) createRouter(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	networkID := mux.Vars(r)["networkId"]
     	var req api.NetworkRouterRequest
     	err = json.NewDecoder(r.Body).Decode(&req)
    @@ -96,13 +89,14 @@ func (h *routersHandler) createRouter(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *routersHandler) getRouter(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	routerID := mux.Vars(r)["routerId"]
     	networkID := mux.Vars(r)["networkId"]
     	router, err := h.routersManager.GetRouter(r.Context(), accountID, userID, networkID, routerID)
    @@ -115,13 +109,14 @@ func (h *routersHandler) getRouter(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *routersHandler) updateRouter(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.NetworkRouterRequest
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -146,13 +141,13 @@ func (h *routersHandler) updateRouter(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *routersHandler) deleteRouter(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	routerID := mux.Vars(r)["routerId"]
     	networkID := mux.Vars(r)["networkId"]
     	err = h.routersManager.DeleteRouter(r.Context(), accountID, userID, networkID, routerID)
    diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go
    index 26153d0a1..709ba64d0 100644
    --- a/management/server/http/handlers/peers/peers_handler.go
    +++ b/management/server/http/handlers/peers/peers_handler.go
    @@ -10,11 +10,10 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/groups"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    @@ -22,12 +21,11 @@ import (
     
     // Handler is a handler that returns peers of the account
     type Handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	peersHandler := NewHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	peersHandler := NewHandler(accountManager)
     	router.HandleFunc("/peers", peersHandler.GetAllPeers).Methods("GET", "OPTIONS")
     	router.HandleFunc("/peers/{peerId}", peersHandler.HandlePeer).
     		Methods("GET", "PUT", "DELETE", "OPTIONS")
    @@ -35,13 +33,9 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
     }
     
     // NewHandler creates a new peers Handler
    -func NewHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *Handler {
    +func NewHandler(accountManager server.AccountManager) *Handler {
     	return &Handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
    @@ -149,12 +143,13 @@ func (h *Handler) deletePeer(ctx context.Context, accountID, userID string, peer
     
     // HandlePeer handles all peer requests for GET, PUT and DELETE operations
     func (h *Handler) HandlePeer(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	peerID := vars["peerId"]
     	if len(peerID) == 0 {
    @@ -179,13 +174,14 @@ func (h *Handler) HandlePeer(w http.ResponseWriter, r *http.Request) {
     
     // GetAllPeers returns a list of all peers associated with a provided account
     func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	peers, err := h.accountManager.GetPeers(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -230,13 +226,14 @@ func (h *Handler) setApprovalRequiredFlag(respBody []*api.PeerBatch, approvedPee
     
     // GetAccessiblePeers returns a list of all peers that the specified peer can connect to within the network.
     func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	peerID := vars["peerId"]
     	if len(peerID) == 0 {
    diff --git a/management/server/http/handlers/peers/peers_handler_test.go b/management/server/http/handlers/peers/peers_handler_test.go
    index 16065a677..63b8c0ab3 100644
    --- a/management/server/http/handlers/peers/peers_handler_test.go
    +++ b/management/server/http/handlers/peers/peers_handler_test.go
    @@ -15,8 +15,8 @@ import (
     	"github.com/gorilla/mux"
     	"golang.org/x/exp/maps"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/types"
     
    @@ -25,16 +25,13 @@ import (
     	"github.com/netbirdio/netbird/management/server/mock_server"
     )
     
    -type ctxKey string
    -
     const (
     	testPeerID                = "test_peer"
     	noUpdateChannelTestPeerID = "no-update-channel"
     
    -	adminUser          = "admin_user"
    -	regularUser        = "regular_user"
    -	serviceUser        = "service_user"
    -	userIDKey   ctxKey = "user_id"
    +	adminUser   = "admin_user"
    +	regularUser = "regular_user"
    +	serviceUser = "service_user"
     )
     
     func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
    @@ -146,9 +143,6 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
     			GetDNSDomainFunc: func() string {
     				return "netbird.selfhosted"
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			GetAccountFunc: func(ctx context.Context, accountID string) (*types.Account, error) {
     				return account, nil
     			},
    @@ -167,16 +161,6 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
     				return ok
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				userID := r.Context().Value(userIDKey).(string)
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    userID,
    -					Domain:    "hotmail.com",
    -					AccountId: "test_id",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -267,8 +251,11 @@ func TestGetPeers(t *testing.T) {
     
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    -			ctx := context.WithValue(context.Background(), userIDKey, "admin_user")
    -			req = req.WithContext(ctx)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "admin_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/peers/", p.GetAllPeers).Methods("GET")
    @@ -412,8 +399,11 @@ func TestGetAccessiblePeers(t *testing.T) {
     
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/peers/%s/accessible-peers", tc.peerID), nil)
    -			ctx := context.WithValue(context.Background(), userIDKey, tc.callerUserID)
    -			req = req.WithContext(ctx)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    tc.callerUserID,
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/peers/{peerId}/accessible-peers", p.GetAccessiblePeers).Methods("GET")
    diff --git a/management/server/http/handlers/policies/geolocation_handler_test.go b/management/server/http/handlers/policies/geolocation_handler_test.go
    index fc5839baa..fbdc324d6 100644
    --- a/management/server/http/handlers/policies/geolocation_handler_test.go
    +++ b/management/server/http/handlers/policies/geolocation_handler_test.go
    @@ -13,9 +13,9 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/types"
     	"github.com/netbirdio/netbird/util"
    @@ -43,23 +43,11 @@ func initGeolocationTestData(t *testing.T) *geolocationsHandler {
     
     	return &geolocationsHandler{
     		accountManager: &mock_server.MockAccountManager{
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) {
     				return types.NewAdminUser(id), nil
     			},
     		},
     		geolocationManager: geo,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_id",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -112,6 +100,11 @@ func TestGetCitiesByCountry(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/locations/countries/{country}/cities", geolocationHandler.getCitiesByCountry).Methods("GET")
    @@ -200,6 +193,11 @@ func TestGetAllCountries(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/locations/countries", geolocationHandler.getAllCountries).Methods("GET")
    diff --git a/management/server/http/handlers/policies/geolocations_handler.go b/management/server/http/handlers/policies/geolocations_handler.go
    index 161d97402..c4868f879 100644
    --- a/management/server/http/handlers/policies/geolocations_handler.go
    +++ b/management/server/http/handlers/policies/geolocations_handler.go
    @@ -7,11 +7,10 @@ import (
     	"github.com/gorilla/mux"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     )
     
    @@ -23,24 +22,19 @@ var (
     type geolocationsHandler struct {
     	accountManager     server.AccountManager
     	geolocationManager geolocation.Geolocation
    -	claimsExtractor    *jwtclaims.ClaimsExtractor
     }
     
    -func addLocationsEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) {
    -	locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager, authCfg)
    +func addLocationsEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) {
    +	locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager)
     	router.HandleFunc("/locations/countries", locationHandler.getAllCountries).Methods("GET", "OPTIONS")
     	router.HandleFunc("/locations/countries/{country}/cities", locationHandler.getCitiesByCountry).Methods("GET", "OPTIONS")
     }
     
     // newGeolocationsHandlerHandler creates a new Geolocations handler
    -func newGeolocationsHandlerHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation, authCfg configs.AuthCfg) *geolocationsHandler {
    +func newGeolocationsHandlerHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation) *geolocationsHandler {
     	return &geolocationsHandler{
     		accountManager:     accountManager,
     		geolocationManager: geolocationManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
    @@ -104,12 +98,13 @@ func (l *geolocationsHandler) getCitiesByCountry(w http.ResponseWriter, r *http.
     }
     
     func (l *geolocationsHandler) authenticateUser(r *http.Request) error {
    -	claims := l.claimsExtractor.FromRequestContext(r)
    -	_, userID, err := l.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		return err
     	}
     
    +	_, userID := userAuth.AccountId, userAuth.UserId
    +
     	user, err := l.accountManager.GetUserByID(r.Context(), userID)
     	if err != nil {
     		return err
    diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go
    index a748e73b8..63fc8a03b 100644
    --- a/management/server/http/handlers/policies/policies_handler.go
    +++ b/management/server/http/handlers/policies/policies_handler.go
    @@ -8,51 +8,46 @@ import (
     	"github.com/gorilla/mux"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // handler is a handler that returns policy of the account
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) {
    -	policiesHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) {
    +	policiesHandler := newHandler(accountManager)
     	router.HandleFunc("/policies", policiesHandler.getAllPolicies).Methods("GET", "OPTIONS")
     	router.HandleFunc("/policies", policiesHandler.createPolicy).Methods("POST", "OPTIONS")
     	router.HandleFunc("/policies/{policyId}", policiesHandler.updatePolicy).Methods("PUT", "OPTIONS")
     	router.HandleFunc("/policies/{policyId}", policiesHandler.getPolicy).Methods("GET", "OPTIONS")
     	router.HandleFunc("/policies/{policyId}", policiesHandler.deletePolicy).Methods("DELETE", "OPTIONS")
    -	addPostureCheckEndpoint(accountManager, locationManager, authCfg, router)
    +	addPostureCheckEndpoint(accountManager, locationManager, router)
     }
     
     // newHandler creates a new policies handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllPolicies list for the account
     func (h *handler) getAllPolicies(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	listPolicies, err := h.accountManager.ListPolicies(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -80,13 +75,14 @@ func (h *handler) getAllPolicies(w http.ResponseWriter, r *http.Request) {
     
     // updatePolicy handles update to a policy identified by a given ID
     func (h *handler) updatePolicy(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	policyID := vars["policyId"]
     	if len(policyID) == 0 {
    @@ -105,13 +101,14 @@ func (h *handler) updatePolicy(w http.ResponseWriter, r *http.Request) {
     
     // createPolicy handles policy creation request
     func (h *handler) createPolicy(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	h.savePolicy(w, r, accountID, userID, "")
     }
     
    @@ -306,13 +303,13 @@ func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID s
     
     // deletePolicy handles policy deletion request
     func (h *handler) deletePolicy(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	policyID := vars["policyId"]
     	if len(policyID) == 0 {
    @@ -330,13 +327,14 @@ func (h *handler) deletePolicy(w http.ResponseWriter, r *http.Request) {
     
     // getPolicy handles a group Get request identified by ID
     func (h *handler) getPolicy(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	policyID := vars["policyId"]
     	if len(policyID) == 0 {
    diff --git a/management/server/http/handlers/policies/policies_handler_test.go b/management/server/http/handlers/policies/policies_handler_test.go
    index 8fbf84d4b..6450295eb 100644
    --- a/management/server/http/handlers/policies/policies_handler_test.go
    +++ b/management/server/http/handlers/policies/policies_handler_test.go
    @@ -13,8 +13,8 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    @@ -44,9 +44,6 @@ func initPoliciesTestData(policies ...*types.Policy) *handler {
     			GetAllGroupsFunc: func(ctx context.Context, accountID, userID string) ([]*types.Group, error) {
     				return []*types.Group{{ID: "F"}, {ID: "G"}}, nil
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			GetAccountByIDFunc: func(ctx context.Context, accountID string, userID string) (*types.Account, error) {
     				user := types.NewAdminUser(userID)
     				return &types.Account{
    @@ -65,15 +62,6 @@ func initPoliciesTestData(policies ...*types.Policy) *handler {
     				}, nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_id",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -115,6 +103,11 @@ func TestPoliciesGetPolicy(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/policies/{policyId}", p.getPolicy).Methods("GET")
    @@ -274,6 +267,11 @@ func TestPoliciesWritePolicy(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/policies", p.createPolicy).Methods("POST")
    diff --git a/management/server/http/handlers/policies/posture_checks_handler.go b/management/server/http/handlers/policies/posture_checks_handler.go
    index ce0d4878c..e6e58da58 100644
    --- a/management/server/http/handlers/policies/posture_checks_handler.go
    +++ b/management/server/http/handlers/policies/posture_checks_handler.go
    @@ -7,11 +7,10 @@ import (
     	"github.com/gorilla/mux"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/status"
     )
    @@ -20,40 +19,35 @@ import (
     type postureChecksHandler struct {
     	accountManager     server.AccountManager
     	geolocationManager geolocation.Geolocation
    -	claimsExtractor    *jwtclaims.ClaimsExtractor
     }
     
    -func addPostureCheckEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) {
    -	postureCheckHandler := newPostureChecksHandler(accountManager, locationManager, authCfg)
    +func addPostureCheckEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) {
    +	postureCheckHandler := newPostureChecksHandler(accountManager, locationManager)
     	router.HandleFunc("/posture-checks", postureCheckHandler.getAllPostureChecks).Methods("GET", "OPTIONS")
     	router.HandleFunc("/posture-checks", postureCheckHandler.createPostureCheck).Methods("POST", "OPTIONS")
     	router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.updatePostureCheck).Methods("PUT", "OPTIONS")
     	router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.getPostureCheck).Methods("GET", "OPTIONS")
     	router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.deletePostureCheck).Methods("DELETE", "OPTIONS")
    -	addLocationsEndpoint(accountManager, locationManager, authCfg, router)
    +	addLocationsEndpoint(accountManager, locationManager, router)
     }
     
     // newPostureChecksHandler creates a new PostureChecks handler
    -func newPostureChecksHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation, authCfg configs.AuthCfg) *postureChecksHandler {
    +func newPostureChecksHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation) *postureChecksHandler {
     	return &postureChecksHandler{
     		accountManager:     accountManager,
     		geolocationManager: geolocationManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllPostureChecks list for the account
     func (p *postureChecksHandler) getAllPostureChecks(w http.ResponseWriter, r *http.Request) {
    -	claims := p.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	listPostureChecks, err := p.accountManager.ListPostureChecks(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -70,13 +64,14 @@ func (p *postureChecksHandler) getAllPostureChecks(w http.ResponseWriter, r *htt
     
     // updatePostureCheck handles update to a posture check identified by a given ID
     func (p *postureChecksHandler) updatePostureCheck(w http.ResponseWriter, r *http.Request) {
    -	claims := p.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	postureChecksID := vars["postureCheckId"]
     	if len(postureChecksID) == 0 {
    @@ -95,25 +90,26 @@ func (p *postureChecksHandler) updatePostureCheck(w http.ResponseWriter, r *http
     
     // createPostureCheck handles posture check creation request
     func (p *postureChecksHandler) createPostureCheck(w http.ResponseWriter, r *http.Request) {
    -	claims := p.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	p.savePostureChecks(w, r, accountID, userID, "")
     }
     
     // getPostureCheck handles a posture check Get request identified by ID
     func (p *postureChecksHandler) getPostureCheck(w http.ResponseWriter, r *http.Request) {
    -	claims := p.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	postureChecksID := vars["postureCheckId"]
     	if len(postureChecksID) == 0 {
    @@ -132,13 +128,13 @@ func (p *postureChecksHandler) getPostureCheck(w http.ResponseWriter, r *http.Re
     
     // deletePostureCheck handles posture check deletion request
     func (p *postureChecksHandler) deletePostureCheck(w http.ResponseWriter, r *http.Request) {
    -	claims := p.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	postureChecksID := vars["postureCheckId"]
     	if len(postureChecksID) == 0 {
    diff --git a/management/server/http/handlers/policies/posture_checks_handler_test.go b/management/server/http/handlers/policies/posture_checks_handler_test.go
    index 237687fd4..e3844caa2 100644
    --- a/management/server/http/handlers/policies/posture_checks_handler_test.go
    +++ b/management/server/http/handlers/policies/posture_checks_handler_test.go
    @@ -14,9 +14,9 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/status"
    @@ -66,20 +66,8 @@ func initPostureChecksTestData(postureChecks ...*posture.Checks) *postureChecksH
     				}
     				return accountPostureChecks, nil
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     		},
     		geolocationManager: &geolocation.Mock{},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_id",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -187,6 +175,11 @@ func TestGetPostureCheck(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(http.MethodGet, "/api/posture-checks/"+tc.id, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/posture-checks/{postureCheckId}", p.getPostureCheck).Methods("GET")
    @@ -835,6 +828,11 @@ func TestPostureCheckUpdate(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			defaultHandler := *p
     			if tc.setupHandlerFunc != nil {
    diff --git a/management/server/http/handlers/routes/routes_handler.go b/management/server/http/handlers/routes/routes_handler.go
    index 6b6c37910..0f0d24780 100644
    --- a/management/server/http/handlers/routes/routes_handler.go
    +++ b/management/server/http/handlers/routes/routes_handler.go
    @@ -10,10 +10,9 @@ import (
     
     	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/route"
     )
    @@ -22,12 +21,11 @@ const failedToConvertRoute = "failed to convert route to response: %v"
     
     // handler is the routes handler of the account
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	routesHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	routesHandler := newHandler(accountManager)
     	router.HandleFunc("/routes", routesHandler.getAllRoutes).Methods("GET", "OPTIONS")
     	router.HandleFunc("/routes", routesHandler.createRoute).Methods("POST", "OPTIONS")
     	router.HandleFunc("/routes/{routeId}", routesHandler.updateRoute).Methods("PUT", "OPTIONS")
    @@ -36,25 +34,22 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
     }
     
     // newHandler returns a new instance of routes handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllRoutes returns the list of routes for the account
     func (h *handler) getAllRoutes(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	routes, err := h.accountManager.ListRoutes(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -75,13 +70,14 @@ func (h *handler) getAllRoutes(w http.ResponseWriter, r *http.Request) {
     
     // createRoute handles route creation request
     func (h *handler) createRoute(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.PostApiRoutesJSONRequestBody
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -172,13 +168,13 @@ func (h *handler) validateRoute(req api.PostApiRoutesJSONRequestBody) error {
     
     // updateRoute handles update to a route identified by a given ID
     func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	routeID := vars["routeId"]
     	if len(routeID) == 0 {
    @@ -265,13 +261,13 @@ func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) {
     
     // deleteRoute handles route deletion request
     func (h *handler) deleteRoute(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	routeID := mux.Vars(r)["routeId"]
     	if len(routeID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid route ID"), w)
    @@ -289,13 +285,14 @@ func (h *handler) deleteRoute(w http.ResponseWriter, r *http.Request) {
     
     // getRoute handles a route Get request identified by ID
     func (h *handler) getRoute(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	routeID := mux.Vars(r)["routeId"]
     	if len(routeID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid route ID"), w)
    diff --git a/management/server/http/handlers/routes/routes_handler_test.go b/management/server/http/handlers/routes/routes_handler_test.go
    index f3bd79ee4..ad1f8912d 100644
    --- a/management/server/http/handlers/routes/routes_handler_test.go
    +++ b/management/server/http/handlers/routes/routes_handler_test.go
    @@ -16,12 +16,10 @@ import (
     	"github.com/stretchr/testify/require"
     
     	"github.com/netbirdio/netbird/management/domain"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
    -	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
    -	"github.com/netbirdio/netbird/management/server/types"
     	"github.com/netbirdio/netbird/management/server/util"
     	"github.com/netbirdio/netbird/route"
     )
    @@ -60,32 +58,6 @@ var baseExistingRoute = &route.Route{
     	Groups:      []string{existingGroupID},
     }
     
    -var testingAccount = &types.Account{
    -	Id:     testAccountID,
    -	Domain: "hotmail.com",
    -	Peers: map[string]*nbpeer.Peer{
    -		existingPeerID: {
    -			Key: existingPeerKey,
    -			IP:  netip.MustParseAddr(existingPeerIP1).AsSlice(),
    -			ID:  existingPeerID,
    -			Meta: nbpeer.PeerSystemMeta{
    -				GoOS: "linux",
    -			},
    -		},
    -		nonLinuxExistingPeerID: {
    -			Key: nonLinuxExistingPeerID,
    -			IP:  netip.MustParseAddr(existingPeerIP2).AsSlice(),
    -			ID:  nonLinuxExistingPeerID,
    -			Meta: nbpeer.PeerSystemMeta{
    -				GoOS: "darwin",
    -			},
    -		},
    -	},
    -	Users: map[string]*types.User{
    -		"test_user": types.NewAdminUser("test_user"),
    -	},
    -}
    -
     func initRoutesTestData() *handler {
     	return &handler{
     		accountManager: &mock_server.MockAccountManager{
    @@ -150,20 +122,7 @@ func initRoutesTestData() *handler {
     				}
     				return nil
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, _ jwtclaims.AuthorizationClaims) (string, string, error) {
    -				// return testingAccount, testingAccount.Users["test_user"], nil
    -				return testingAccount.Id, testingAccount.Users["test_user"].Id, nil
    -			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: testAccountID,
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -526,6 +485,11 @@ func TestRoutesHandlers(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: testAccountID,
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/routes/{routeId}", p.getRoute).Methods("GET")
    diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler.go b/management/server/http/handlers/setup_keys/setupkeys_handler.go
    index 3bd3ef589..8095f43b0 100644
    --- a/management/server/http/handlers/setup_keys/setupkeys_handler.go
    +++ b/management/server/http/handlers/setup_keys/setupkeys_handler.go
    @@ -10,22 +10,20 @@ import (
     	"github.com/gorilla/mux"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // handler is a handler that returns a list of setup keys of the account
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	keysHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	keysHandler := newHandler(accountManager)
     	router.HandleFunc("/setup-keys", keysHandler.getAllSetupKeys).Methods("GET", "OPTIONS")
     	router.HandleFunc("/setup-keys", keysHandler.createSetupKey).Methods("POST", "OPTIONS")
     	router.HandleFunc("/setup-keys/{keyId}", keysHandler.getSetupKey).Methods("GET", "OPTIONS")
    @@ -34,25 +32,21 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
     }
     
     // newHandler creates a new setup key handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // createSetupKey is a POST requests that creates a new SetupKey
     func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	req := &api.PostApiSetupKeysJSONRequestBody{}
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -108,12 +102,12 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
     
     // getSetupKey is a GET request to get a SetupKey by ID
     func (h *handler) getSetupKey(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	vars := mux.Vars(r)
     	keyID := vars["keyId"]
    @@ -133,13 +127,13 @@ func (h *handler) getSetupKey(w http.ResponseWriter, r *http.Request) {
     
     // updateSetupKey is a PUT request to update server.SetupKey
     func (h *handler) updateSetupKey(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	keyID := vars["keyId"]
     	if len(keyID) == 0 {
    @@ -174,13 +168,13 @@ func (h *handler) updateSetupKey(w http.ResponseWriter, r *http.Request) {
     
     // getAllSetupKeys is a GET request that returns a list of SetupKey
     func (h *handler) getAllSetupKeys(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	setupKeys, err := h.accountManager.ListSetupKeys(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -196,13 +190,13 @@ func (h *handler) getAllSetupKeys(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *handler) deleteSetupKey(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	keyID := vars["keyId"]
     	if len(keyID) == 0 {
    diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    index 4912f9639..e9135469f 100644
    --- a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    +++ b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    @@ -14,8 +14,8 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    @@ -28,14 +28,9 @@ const (
     	notFoundSetupKeyID  = "notFoundSetupKeyID"
     )
     
    -func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKey, updatedSetupKey *types.SetupKey,
    -	user *types.User,
    -) *handler {
    +func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKey, updatedSetupKey *types.SetupKey) *handler {
     	return &handler{
     		accountManager: &mock_server.MockAccountManager{
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			CreateSetupKeyFunc: func(_ context.Context, _ string, keyName string, typ types.SetupKeyType, _ time.Duration, _ []string,
     				_ int, _ string, ephemeral bool, allowExtraDNSLabels bool,
     			) (*types.SetupKey, error) {
    @@ -76,15 +71,6 @@ func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKe
     				return status.Errorf(status.NotFound, "key %s not found", keyID)
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    user.Id,
    -					Domain:    "hotmail.com",
    -					AccountId: "testAccountId",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -171,12 +157,17 @@ func TestSetupKeysHandlers(t *testing.T) {
     		},
     	}
     
    -	handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey, adminUser)
    +	handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey)
     
     	for _, tc := range tt {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    adminUser.Id,
    +				Domain:    "hotmail.com",
    +				AccountId: "testAccountId",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/setup-keys", handler.getAllSetupKeys).Methods("GET", "OPTIONS")
    diff --git a/management/server/http/handlers/users/pat_handler.go b/management/server/http/handlers/users/pat_handler.go
    index 7b93d2ae1..84fbef93e 100644
    --- a/management/server/http/handlers/users/pat_handler.go
    +++ b/management/server/http/handlers/users/pat_handler.go
    @@ -7,22 +7,20 @@ import (
     	"github.com/gorilla/mux"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // patHandler is the nameserver group handler of the account
     type patHandler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func addUsersTokensEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	tokenHandler := newPATsHandler(accountManager, authCfg)
    +func addUsersTokensEndpoint(accountManager server.AccountManager, router *mux.Router) {
    +	tokenHandler := newPATsHandler(accountManager)
     	router.HandleFunc("/users/{userId}/tokens", tokenHandler.getAllTokens).Methods("GET", "OPTIONS")
     	router.HandleFunc("/users/{userId}/tokens", tokenHandler.createToken).Methods("POST", "OPTIONS")
     	router.HandleFunc("/users/{userId}/tokens/{tokenId}", tokenHandler.getToken).Methods("GET", "OPTIONS")
    @@ -30,25 +28,21 @@ func addUsersTokensEndpoint(accountManager server.AccountManager, authCfg config
     }
     
     // newPATsHandler creates a new patHandler HTTP handler
    -func newPATsHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *patHandler {
    +func newPATsHandler(accountManager server.AccountManager) *patHandler {
     	return &patHandler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllTokens is HTTP GET handler that returns a list of all personal access tokens for the given user
     func (h *patHandler) getAllTokens(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(userID) == 0 {
    @@ -72,13 +66,13 @@ func (h *patHandler) getAllTokens(w http.ResponseWriter, r *http.Request) {
     
     // getToken is HTTP GET handler that returns a personal access token for the given user
     func (h *patHandler) getToken(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(targetUserID) == 0 {
    @@ -103,13 +97,13 @@ func (h *patHandler) getToken(w http.ResponseWriter, r *http.Request) {
     
     // createToken is HTTP POST handler that creates a personal access token for the given user
     func (h *patHandler) createToken(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(targetUserID) == 0 {
    @@ -135,13 +129,13 @@ func (h *patHandler) createToken(w http.ResponseWriter, r *http.Request) {
     
     // deleteToken is HTTP DELETE handler that deletes a personal access token for the given user
     func (h *patHandler) deleteToken(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(targetUserID) == 0 {
    diff --git a/management/server/http/handlers/users/pat_handler_test.go b/management/server/http/handlers/users/pat_handler_test.go
    index 9388067a4..6593de64a 100644
    --- a/management/server/http/handlers/users/pat_handler_test.go
    +++ b/management/server/http/handlers/users/pat_handler_test.go
    @@ -12,11 +12,12 @@ import (
     
     	"github.com/google/go-cmp/cmp"
     	"github.com/gorilla/mux"
    -	"github.com/netbirdio/netbird/management/server/util"
     	"github.com/stretchr/testify/assert"
     
    +	"github.com/netbirdio/netbird/management/server/util"
    +
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    @@ -77,10 +78,6 @@ func initPATTestData() *patHandler {
     					PersonalAccessToken: types.PersonalAccessToken{},
     				}, nil
     			},
    -
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			DeletePATFunc: func(_ context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error {
     				if accountID != existingAccountID {
     					return status.Errorf(status.NotFound, "account with ID %s not found", accountID)
    @@ -115,15 +112,6 @@ func initPATTestData() *patHandler {
     				return []*types.PersonalAccessToken{testAccount.Users[existingUserID].PATs[existingTokenID], testAccount.Users[existingUserID].PATs["token2"]}, nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    existingUserID,
    -					Domain:    testDomain,
    -					AccountId: existingAccountID,
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -185,6 +173,11 @@ func TestTokenHandlers(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/users/{userId}/tokens", p.getAllTokens).Methods("GET")
    diff --git a/management/server/http/handlers/users/users_handler.go b/management/server/http/handlers/users/users_handler.go
    index 7380dd97e..3869f21f0 100644
    --- a/management/server/http/handlers/users/users_handler.go
    +++ b/management/server/http/handlers/users/users_handler.go
    @@ -9,39 +9,33 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     
     	"github.com/netbirdio/netbird/management/server"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     )
     
     // handler is a handler that returns users of the account
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	userHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	userHandler := newHandler(accountManager)
     	router.HandleFunc("/users", userHandler.getAllUsers).Methods("GET", "OPTIONS")
     	router.HandleFunc("/users/{userId}", userHandler.updateUser).Methods("PUT", "OPTIONS")
     	router.HandleFunc("/users/{userId}", userHandler.deleteUser).Methods("DELETE", "OPTIONS")
     	router.HandleFunc("/users", userHandler.createUser).Methods("POST", "OPTIONS")
     	router.HandleFunc("/users/{userId}/invite", userHandler.inviteUser).Methods("POST", "OPTIONS")
    -	addUsersTokensEndpoint(accountManager, authCfg, router)
    +	addUsersTokensEndpoint(accountManager, router)
     }
     
     // newHandler creates a new UsersHandler HTTP handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
    @@ -52,13 +46,13 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(targetUserID) == 0 {
    @@ -103,7 +97,7 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    -	util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, claims.UserId))
    +	util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, userID))
     }
     
     // deleteUser is a DELETE request to delete a user
    @@ -113,13 +107,13 @@ func (h *handler) deleteUser(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(targetUserID) == 0 {
    @@ -143,12 +137,12 @@ func (h *handler) createUser(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	req := &api.PostApiUsersJSONRequestBody{}
     	err = json.NewDecoder(r.Body).Decode(&req)
    @@ -184,7 +178,7 @@ func (h *handler) createUser(w http.ResponseWriter, r *http.Request) {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    -	util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, claims.UserId))
    +	util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, userID))
     }
     
     // getAllUsers returns a list of users of the account this user belongs to.
    @@ -195,13 +189,13 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	data, err := h.accountManager.GetUsersFromAccount(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -216,7 +210,7 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
     			continue
     		}
     		if serviceUser == "" {
    -			users = append(users, toUserResponse(d, claims.UserId))
    +			users = append(users, toUserResponse(d, userID))
     			continue
     		}
     
    @@ -227,7 +221,7 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
     			return
     		}
     		if includeServiceUser == d.IsServiceUser {
    -			users = append(users, toUserResponse(d, claims.UserId))
    +			users = append(users, toUserResponse(d, userID))
     		}
     	}
     
    @@ -242,12 +236,12 @@ func (h *handler) inviteUser(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
    diff --git a/management/server/http/handlers/users/users_handler_test.go b/management/server/http/handlers/users/users_handler_test.go
    index ff77cedff..a6a904a4c 100644
    --- a/management/server/http/handlers/users/users_handler_test.go
    +++ b/management/server/http/handlers/users/users_handler_test.go
    @@ -13,8 +13,8 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    @@ -64,9 +64,6 @@ var usersTestAccount = &types.Account{
     func initUsersTestData() *handler {
     	return &handler{
     		accountManager: &mock_server.MockAccountManager{
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return usersTestAccount.Id, claims.UserId, nil
    -			},
     			GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) {
     				return usersTestAccount.Users[id], nil
     			},
    @@ -127,15 +124,6 @@ func initUsersTestData() *handler {
     				return nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    existingUserID,
    -					Domain:    testDomain,
    -					AccountId: existingAccountID,
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -158,6 +146,11 @@ func TestGetUsers(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
     
     			userHandler.getAllUsers(recorder, req)
     
    @@ -263,6 +256,11 @@ func TestUpdateUser(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/users/{userId}", userHandler.updateUser).Methods("PUT")
    @@ -355,6 +353,11 @@ func TestCreateUser(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
     			rr := httptest.NewRecorder()
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
     
     			userHandler.createUser(rr, req)
     
    @@ -399,6 +402,12 @@ func TestInviteUser(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
     			req = mux.SetURLVars(req, tc.requestVars)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
    +
     			rr := httptest.NewRecorder()
     
     			userHandler.inviteUser(rr, req)
    @@ -452,6 +461,12 @@ func TestDeleteUser(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
     			req = mux.SetURLVars(req, tc.requestVars)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
    +
     			rr := httptest.NewRecorder()
     
     			userHandler.deleteUser(rr, req)
    diff --git a/management/server/http/middleware/access_control.go b/management/server/http/middleware/access_control.go
    index c5bdf5fe7..4ed90f47b 100644
    --- a/management/server/http/middleware/access_control.go
    +++ b/management/server/http/middleware/access_control.go
    @@ -7,30 +7,24 @@ import (
     
     	log "github.com/sirupsen/logrus"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
     	"github.com/netbirdio/netbird/management/server/http/util"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    -
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     )
     
     // GetUser function defines a function to fetch user from Account by jwtclaims.AuthorizationClaims
    -type GetUser func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error)
    +type GetUser func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
     
     // AccessControl middleware to restrict to make POST/PUT/DELETE requests by admin only
     type AccessControl struct {
    -	claimsExtract jwtclaims.ClaimsExtractor
    -	getUser       GetUser
    +	getUser GetUser
     }
     
     // NewAccessControl instance constructor
    -func NewAccessControl(audience, userIDClaim string, getUser GetUser) *AccessControl {
    +func NewAccessControl(getUser GetUser) *AccessControl {
     	return &AccessControl{
    -		claimsExtract: *jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(audience),
    -			jwtclaims.WithUserIDClaim(userIDClaim),
    -		),
     		getUser: getUser,
     	}
     }
    @@ -45,12 +39,16 @@ func (a *AccessControl) Handler(h http.Handler) http.Handler {
     			return
     		}
     
    -		claims := a.claimsExtract.FromRequestContext(r)
    -
    -		user, err := a.getUser(r.Context(), claims)
    +		userAuth, err := nbcontext.GetUserAuthFromRequest(r)
     		if err != nil {
    -			log.WithContext(r.Context()).Errorf("failed to get user from claims: %s", err)
    -			util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid JWT"), w)
    +			log.WithContext(r.Context()).Errorf("failed to get user auth from request: %s", err)
    +			util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid user auth"), w)
    +		}
    +
    +		user, err := a.getUser(r.Context(), userAuth)
    +		if err != nil {
    +			log.WithContext(r.Context()).Errorf("failed to get user: %s", err)
    +			util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid user auth"), w)
     			return
     		}
     
    diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go
    index dcf73259a..a8e6790a9 100644
    --- a/management/server/http/middleware/auth_middleware.go
    +++ b/management/server/http/middleware/auth_middleware.go
    @@ -8,67 +8,41 @@ import (
     	"strings"
     	"time"
     
    -	"github.com/golang-jwt/jwt"
     	log "github.com/sirupsen/logrus"
     
    -	nbContext "github.com/netbirdio/netbird/management/server/context"
    +	"github.com/netbirdio/netbird/management/server/auth"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
    -	"github.com/netbirdio/netbird/management/server/types"
     )
     
    -// GetAccountInfoFromPATFunc function
    -type GetAccountInfoFromPATFunc func(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
    -
    -// ValidateAndParseTokenFunc function
    -type ValidateAndParseTokenFunc func(ctx context.Context, token string) (*jwt.Token, error)
    -
    -// MarkPATUsedFunc function
    -type MarkPATUsedFunc func(ctx context.Context, token string) error
    -
    -// CheckUserAccessByJWTGroupsFunc function
    -type CheckUserAccessByJWTGroupsFunc func(ctx context.Context, claims jwtclaims.AuthorizationClaims) error
    +type EnsureAccountFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
    +type SyncUserJWTGroupsFunc func(ctx context.Context, userAuth nbcontext.UserAuth) error
     
     // AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens
     type AuthMiddleware struct {
    -	getAccountInfoFromPAT      GetAccountInfoFromPATFunc
    -	validateAndParseToken      ValidateAndParseTokenFunc
    -	markPATUsed                MarkPATUsedFunc
    -	checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc
    -	claimsExtractor            *jwtclaims.ClaimsExtractor
    -	audience                   string
    -	userIDClaim                string
    +	authManager       auth.Manager
    +	ensureAccount     EnsureAccountFunc
    +	syncUserJWTGroups SyncUserJWTGroupsFunc
     }
     
    -const (
    -	userProperty = "user"
    -)
    -
     // NewAuthMiddleware instance constructor
    -func NewAuthMiddleware(getAccountInfoFromPAT GetAccountInfoFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc,
    -	markPATUsed MarkPATUsedFunc, checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc, claimsExtractor *jwtclaims.ClaimsExtractor,
    -	audience string, userIdClaim string) *AuthMiddleware {
    -	if userIdClaim == "" {
    -		userIdClaim = jwtclaims.UserIDClaim
    -	}
    -
    +func NewAuthMiddleware(
    +	authManager auth.Manager,
    +	ensureAccount EnsureAccountFunc,
    +	syncUserJWTGroups SyncUserJWTGroupsFunc,
    +) *AuthMiddleware {
     	return &AuthMiddleware{
    -		getAccountInfoFromPAT:      getAccountInfoFromPAT,
    -		validateAndParseToken:      validateAndParseToken,
    -		markPATUsed:                markPATUsed,
    -		checkUserAccessByJWTGroups: checkUserAccessByJWTGroups,
    -		claimsExtractor:            claimsExtractor,
    -		audience:                   audience,
    -		userIDClaim:                userIdClaim,
    +		authManager:       authManager,
    +		ensureAccount:     ensureAccount,
    +		syncUserJWTGroups: syncUserJWTGroups,
     	}
     }
     
     // Handler method of the middleware which authenticates a user either by JWT claims or by PAT
     func (m *AuthMiddleware) Handler(h http.Handler) http.Handler {
     	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    -
     		if bypass.ShouldBypass(r.URL.Path, h, w, r) {
     			return
     		}
    @@ -84,108 +58,111 @@ func (m *AuthMiddleware) Handler(h http.Handler) http.Handler {
     
     		switch authType {
     		case "bearer":
    -			err := m.checkJWTFromRequest(w, r, auth)
    +			request, err := m.checkJWTFromRequest(r, auth)
     			if err != nil {
    -				log.WithContext(r.Context()).Errorf("Error when validating JWT claims: %s", err.Error())
    +				log.WithContext(r.Context()).Errorf("Error when validating JWT: %s", err.Error())
     				util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "token invalid"), w)
     				return
     			}
    +
    +			h.ServeHTTP(w, request)
     		case "token":
    -			err := m.checkPATFromRequest(w, r, auth)
    +			request, err := m.checkPATFromRequest(r, auth)
     			if err != nil {
    -				log.WithContext(r.Context()).Debugf("Error when validating PAT claims: %s", err.Error())
    +				log.WithContext(r.Context()).Debugf("Error when validating PAT: %s", err.Error())
     				util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "token invalid"), w)
     				return
     			}
    +			h.ServeHTTP(w, request)
     		default:
     			util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "no valid authentication provided"), w)
     			return
     		}
    -		claims := m.claimsExtractor.FromRequestContext(r)
    -		//nolint
    -		ctx := context.WithValue(r.Context(), nbContext.UserIDKey, claims.UserId)
    -		//nolint
    -		ctx = context.WithValue(ctx, nbContext.AccountIDKey, claims.AccountId)
    -		h.ServeHTTP(w, r.WithContext(ctx))
     	})
     }
     
     // CheckJWTFromRequest checks if the JWT is valid
    -func (m *AuthMiddleware) checkJWTFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error {
    +func (m *AuthMiddleware) checkJWTFromRequest(r *http.Request, auth []string) (*http.Request, error) {
     	token, err := getTokenFromJWTRequest(auth)
     
     	// If an error occurs, call the error handler and return an error
     	if err != nil {
    -		return fmt.Errorf("Error extracting token: %w", err)
    +		return r, fmt.Errorf("error extracting token: %w", err)
     	}
     
    -	validatedToken, err := m.validateAndParseToken(r.Context(), token)
    +	ctx := r.Context()
    +
    +	userAuth, validatedToken, err := m.authManager.ValidateAndParseToken(ctx, token)
     	if err != nil {
    -		return err
    +		return r, err
     	}
     
    -	if validatedToken == nil {
    -		return nil
    +	if impersonate, ok := r.URL.Query()["account"]; ok && len(impersonate) == 1 {
    +		userAuth.AccountId = impersonate[0]
    +		userAuth.IsChild = ok
     	}
     
    -	if err := m.verifyUserAccess(r.Context(), validatedToken); err != nil {
    -		return err
    +	// we need to call this method because if user is new, we will automatically add it to existing or create a new account
    +	accountId, _, err := m.ensureAccount(ctx, userAuth)
    +	if err != nil {
    +		return r, err
     	}
     
    -	// If we get here, everything worked and we can set the
    -	// user property in context.
    -	newRequest := r.WithContext(context.WithValue(r.Context(), userProperty, validatedToken)) //nolint
    -	// Update the current request with the new context information.
    -	*r = *newRequest
    -	return nil
    -}
    +	if userAuth.AccountId != accountId {
    +		log.WithContext(ctx).Debugf("Auth middleware sets accountId from ensure, before %s, now %s", userAuth.AccountId, accountId)
    +		userAuth.AccountId = accountId
    +	}
     
    -// verifyUserAccess checks if a user, based on a validated JWT token,
    -// is allowed access, particularly in cases where the admin enabled JWT
    -// group propagation and designated certain groups with access permissions.
    -func (m *AuthMiddleware) verifyUserAccess(ctx context.Context, validatedToken *jwt.Token) error {
    -	authClaims := m.claimsExtractor.FromToken(validatedToken)
    -	return m.checkUserAccessByJWTGroups(ctx, authClaims)
    +	userAuth, err = m.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, validatedToken)
    +	if err != nil {
    +		return r, err
    +	}
    +
    +	err = m.syncUserJWTGroups(ctx, userAuth)
    +	if err != nil {
    +		log.WithContext(ctx).Errorf("HTTP server failed to sync user JWT groups: %s", err)
    +	}
    +
    +	return nbcontext.SetUserAuthInRequest(r, userAuth), nil
     }
     
     // CheckPATFromRequest checks if the PAT is valid
    -func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error {
    +func (m *AuthMiddleware) checkPATFromRequest(r *http.Request, auth []string) (*http.Request, error) {
     	token, err := getTokenFromPATRequest(auth)
     	if err != nil {
    -		return fmt.Errorf("error extracting token: %w", err)
    +		return r, fmt.Errorf("error extracting token: %w", err)
     	}
     
    -	user, pat, accDomain, accCategory, err := m.getAccountInfoFromPAT(r.Context(), token)
    +	ctx := r.Context()
    +	user, pat, accDomain, accCategory, err := m.authManager.GetPATInfo(ctx, token)
     	if err != nil {
    -		return fmt.Errorf("invalid Token: %w", err)
    +		return r, fmt.Errorf("invalid Token: %w", err)
     	}
     	if time.Now().After(pat.GetExpirationDate()) {
    -		return fmt.Errorf("token expired")
    +		return r, fmt.Errorf("token expired")
     	}
     
    -	err = m.markPATUsed(r.Context(), pat.ID)
    +	err = m.authManager.MarkPATUsed(ctx, pat.ID)
     	if err != nil {
    -		return err
    +		return r, err
     	}
     
    -	claimMaps := jwt.MapClaims{}
    -	claimMaps[m.userIDClaim] = user.Id
    -	claimMaps[m.audience+jwtclaims.AccountIDSuffix] = user.AccountID
    -	claimMaps[m.audience+jwtclaims.DomainIDSuffix] = accDomain
    -	claimMaps[m.audience+jwtclaims.DomainCategorySuffix] = accCategory
    -	claimMaps[jwtclaims.IsToken] = true
    -	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
    -	newRequest := r.WithContext(context.WithValue(r.Context(), jwtclaims.TokenUserProperty, jwtToken)) //nolint
    -	// Update the current request with the new context information.
    -	*r = *newRequest
    -	return nil
    +	userAuth := nbcontext.UserAuth{
    +		UserId:         user.Id,
    +		AccountId:      user.AccountID,
    +		Domain:         accDomain,
    +		DomainCategory: accCategory,
    +		IsPAT:          true,
    +	}
    +
    +	return nbcontext.SetUserAuthInRequest(r, userAuth), nil
     }
     
     // getTokenFromJWTRequest is a "TokenExtractor" that takes auth header parts and extracts
     // the JWT token from the Authorization header.
     func getTokenFromJWTRequest(authHeaderParts []string) (string, error) {
     	if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
    -		return "", errors.New("Authorization header format must be Bearer {token}")
    +		return "", errors.New("authorization header format must be Bearer {token}")
     	}
     
     	return authHeaderParts[1], nil
    @@ -195,7 +172,7 @@ func getTokenFromJWTRequest(authHeaderParts []string) (string, error) {
     // the PAT token from the Authorization header.
     func getTokenFromPATRequest(authHeaderParts []string) (string, error) {
     	if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "token" {
    -		return "", errors.New("Authorization header format must be Token {token}")
    +		return "", errors.New("authorization header format must be Token {token}")
     	}
     
     	return authHeaderParts[1], nil
    diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go
    index c1686ed44..3dc7d51cb 100644
    --- a/management/server/http/middleware/auth_middleware_test.go
    +++ b/management/server/http/middleware/auth_middleware_test.go
    @@ -9,10 +9,14 @@ import (
     	"time"
     
     	"github.com/golang-jwt/jwt"
    +	"github.com/stretchr/testify/assert"
    +
    +	"github.com/netbirdio/netbird/management/server/auth"
    +	nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/util"
     
     	"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
    @@ -58,17 +62,23 @@ func mockGetAccountInfoFromPAT(_ context.Context, token string) (user *types.Use
     	return nil, nil, "", "", fmt.Errorf("PAT invalid")
     }
     
    -func mockValidateAndParseToken(_ context.Context, token string) (*jwt.Token, error) {
    +func mockValidateAndParseToken(_ context.Context, token string) (nbcontext.UserAuth, *jwt.Token, error) {
     	if token == JWT {
    -		return &jwt.Token{
    -			Claims: jwt.MapClaims{
    -				userIDClaim:                          userID,
    -				audience + jwtclaims.AccountIDSuffix: accountID,
    +		return nbcontext.UserAuth{
    +				UserId:         userID,
    +				AccountId:      accountID,
    +				Domain:         testAccount.Domain,
    +				DomainCategory: testAccount.DomainCategory,
     			},
    -			Valid: true,
    -		}, nil
    +			&jwt.Token{
    +				Claims: jwt.MapClaims{
    +					userIDClaim:                      userID,
    +					audience + nbjwt.AccountIDSuffix: accountID,
    +				},
    +				Valid: true,
    +			}, nil
     	}
    -	return nil, fmt.Errorf("JWT invalid")
    +	return nbcontext.UserAuth{}, nil, fmt.Errorf("JWT invalid")
     }
     
     func mockMarkPATUsed(_ context.Context, token string) error {
    @@ -78,16 +88,20 @@ func mockMarkPATUsed(_ context.Context, token string) error {
     	return fmt.Errorf("Should never get reached")
     }
     
    -func mockCheckUserAccessByJWTGroups(_ context.Context, claims jwtclaims.AuthorizationClaims) error {
    -	if testAccount.Id != claims.AccountId {
    -		return fmt.Errorf("account with id %s does not exist", claims.AccountId)
    +func mockEnsureUserAccessByJWTGroups(_ context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) {
    +	if userAuth.IsChild || userAuth.IsPAT {
    +		return userAuth, nil
     	}
     
    -	if _, ok := testAccount.Users[claims.UserId]; !ok {
    -		return fmt.Errorf("user with id %s does not exist", claims.UserId)
    +	if testAccount.Id != userAuth.AccountId {
    +		return userAuth, fmt.Errorf("account with id %s does not exist", userAuth.AccountId)
     	}
     
    -	return nil
    +	if _, ok := testAccount.Users[userAuth.UserId]; !ok {
    +		return userAuth, fmt.Errorf("user with id %s does not exist", userAuth.UserId)
    +	}
    +
    +	return userAuth, nil
     }
     
     func TestAuthMiddleware_Handler(t *testing.T) {
    @@ -158,22 +172,24 @@ func TestAuthMiddleware_Handler(t *testing.T) {
     	}
     
     	nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    -		// do nothing
    +
     	})
     
    -	claimsExtractor := jwtclaims.NewClaimsExtractor(
    -		jwtclaims.WithAudience(audience),
    -		jwtclaims.WithUserIDClaim(userIDClaim),
    -	)
    +	mockAuth := &auth.MockManager{
    +		ValidateAndParseTokenFunc:       mockValidateAndParseToken,
    +		EnsureUserAccessByJWTGroupsFunc: mockEnsureUserAccessByJWTGroups,
    +		MarkPATUsedFunc:                 mockMarkPATUsed,
    +		GetPATInfoFunc:                  mockGetAccountInfoFromPAT,
    +	}
     
     	authMiddleware := NewAuthMiddleware(
    -		mockGetAccountInfoFromPAT,
    -		mockValidateAndParseToken,
    -		mockMarkPATUsed,
    -		mockCheckUserAccessByJWTGroups,
    -		claimsExtractor,
    -		audience,
    -		userIDClaim,
    +		mockAuth,
    +		func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
    +			return userAuth.AccountId, userAuth.UserId, nil
    +		},
    +		func(ctx context.Context, userAuth nbcontext.UserAuth) error {
    +			return nil
    +		},
     	)
     
     	handlerToTest := authMiddleware.Handler(nextHandler)
    @@ -195,9 +211,115 @@ func TestAuthMiddleware_Handler(t *testing.T) {
     
     			result := rec.Result()
     			defer result.Body.Close()
    +
     			if result.StatusCode != tc.expectedStatusCode {
     				t.Errorf("expected status code %d, got %d", tc.expectedStatusCode, result.StatusCode)
     			}
     		})
     	}
     }
    +
    +func TestAuthMiddleware_Handler_Child(t *testing.T) {
    +	tt := []struct {
    +		name             string
    +		path             string
    +		authHeader       string
    +		expectedUserAuth *nbcontext.UserAuth // nil expects 401 response status
    +	}{
    +		{
    +			name:       "Valid PAT Token",
    +			path:       "/test",
    +			authHeader: "Token " + PAT,
    +			expectedUserAuth: &nbcontext.UserAuth{
    +				AccountId:      accountID,
    +				UserId:         userID,
    +				Domain:         testAccount.Domain,
    +				DomainCategory: testAccount.DomainCategory,
    +				IsPAT:          true,
    +			},
    +		},
    +		{
    +			name:       "Valid PAT Token ignores child",
    +			path:       "/test?account=xyz",
    +			authHeader: "Token " + PAT,
    +			expectedUserAuth: &nbcontext.UserAuth{
    +				AccountId:      accountID,
    +				UserId:         userID,
    +				Domain:         testAccount.Domain,
    +				DomainCategory: testAccount.DomainCategory,
    +				IsPAT:          true,
    +			},
    +		},
    +		{
    +			name:       "Valid JWT Token",
    +			path:       "/test",
    +			authHeader: "Bearer " + JWT,
    +			expectedUserAuth: &nbcontext.UserAuth{
    +				AccountId:      accountID,
    +				UserId:         userID,
    +				Domain:         testAccount.Domain,
    +				DomainCategory: testAccount.DomainCategory,
    +			},
    +		},
    +
    +		{
    +			name:       "Valid JWT Token with child",
    +			path:       "/test?account=xyz",
    +			authHeader: "Bearer " + JWT,
    +			expectedUserAuth: &nbcontext.UserAuth{
    +				AccountId:      "xyz",
    +				UserId:         userID,
    +				Domain:         testAccount.Domain,
    +				DomainCategory: testAccount.DomainCategory,
    +				IsChild:        true,
    +			},
    +		},
    +	}
    +
    +	mockAuth := &auth.MockManager{
    +		ValidateAndParseTokenFunc:       mockValidateAndParseToken,
    +		EnsureUserAccessByJWTGroupsFunc: mockEnsureUserAccessByJWTGroups,
    +		MarkPATUsedFunc:                 mockMarkPATUsed,
    +		GetPATInfoFunc:                  mockGetAccountInfoFromPAT,
    +	}
    +
    +	authMiddleware := NewAuthMiddleware(
    +		mockAuth,
    +		func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
    +			return userAuth.AccountId, userAuth.UserId, nil
    +		},
    +		func(ctx context.Context, userAuth nbcontext.UserAuth) error {
    +			return nil
    +		},
    +	)
    +
    +	for _, tc := range tt {
    +		t.Run(tc.name, func(t *testing.T) {
    +			handlerToTest := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +				userAuth, err := nbcontext.GetUserAuthFromRequest(r)
    +				if tc.expectedUserAuth != nil {
    +					assert.NoError(t, err)
    +					assert.Equal(t, *tc.expectedUserAuth, userAuth)
    +				} else {
    +					assert.Error(t, err)
    +					assert.Empty(t, userAuth)
    +				}
    +			}))
    +
    +			req := httptest.NewRequest("GET", "http://testing"+tc.path, nil)
    +			req.Header.Set("Authorization", tc.authHeader)
    +			rec := httptest.NewRecorder()
    +
    +			handlerToTest.ServeHTTP(rec, req)
    +
    +			result := rec.Result()
    +			defer result.Body.Close()
    +
    +			if tc.expectedUserAuth != nil {
    +				assert.Equal(t, 200, result.StatusCode)
    +			} else {
    +				assert.Equal(t, 401, result.StatusCode)
    +			}
    +		})
    +	}
    +}
    diff --git a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
    index 7f8eee6e7..e2c2c1d85 100644
    --- a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
    +++ b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
    @@ -77,13 +77,13 @@ func BenchmarkUpdatePeer(b *testing.B) {
     
     func BenchmarkGetOnePeer(b *testing.B) {
     	var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
    -		"Peers - XS":     {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 70},
    -		"Peers - S":      {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 30},
    -		"Peers - M":      {MinMsPerOpLocal: 9, MaxMsPerOpLocal: 18, MinMsPerOpCICD: 15, MaxMsPerOpCICD: 50},
    -		"Peers - L":      {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130},
    -		"Groups - L":     {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 200},
    -		"Users - L":      {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130},
    -		"Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130},
    +		"Peers - XS":     {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
    +		"Peers - S":      {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
    +		"Peers - M":      {MinMsPerOpLocal: 9, MaxMsPerOpLocal: 18, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
    +		"Peers - L":      {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
    +		"Groups - L":     {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
    +		"Users - L":      {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
    +		"Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
     		"Peers - XL":     {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 400, MinMsPerOpCICD: 200, MaxMsPerOpCICD: 750},
     	}
     
    @@ -111,9 +111,9 @@ func BenchmarkGetOnePeer(b *testing.B) {
     
     func BenchmarkGetAllPeers(b *testing.B) {
     	var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
    -		"Peers - XS":     {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 150},
    -		"Peers - S":      {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 30},
    -		"Peers - M":      {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 70},
    +		"Peers - XS":     {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
    +		"Peers - S":      {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
    +		"Peers - M":      {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
     		"Peers - L":      {MinMsPerOpLocal: 110, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300},
     		"Groups - L":     {MinMsPerOpLocal: 150, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 130, MaxMsPerOpCICD: 500},
     		"Users - L":      {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 170, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 400},
    diff --git a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
    index 0baf76328..b7deab334 100644
    --- a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
    +++ b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
    @@ -48,13 +48,12 @@ func BenchmarkUpdateUser(b *testing.B) {
     	log.SetOutput(io.Discard)
     	defer log.SetOutput(os.Stderr)
     
    -	recorder := httptest.NewRecorder()
    -
     	for name, bc := range benchCasesUsers {
     		b.Run(name, func(b *testing.B) {
     			apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
     			testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys)
     
    +			recorder := httptest.NewRecorder()
     			b.ResetTimer()
     			start := time.Now()
     			for i := 0; i < b.N; i++ {
    @@ -97,13 +96,12 @@ func BenchmarkGetOneUser(b *testing.B) {
     	log.SetOutput(io.Discard)
     	defer log.SetOutput(os.Stderr)
     
    -	recorder := httptest.NewRecorder()
    -
     	for name, bc := range benchCasesUsers {
     		b.Run(name, func(b *testing.B) {
     			apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
     			testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys)
     
    +			recorder := httptest.NewRecorder()
     			b.ResetTimer()
     			start := time.Now()
     			for i := 0; i < b.N; i++ {
    @@ -118,26 +116,25 @@ func BenchmarkGetOneUser(b *testing.B) {
     
     func BenchmarkGetAllUsers(b *testing.B) {
     	var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
    -		"Users - XS":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10},
    -		"Users - S":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10},
    -		"Users - M":      {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 15},
    -		"Users - L":      {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 50},
    -		"Peers - L":      {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 55},
    -		"Groups - L":     {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55},
    -		"Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55},
    -		"Users - XL":     {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300},
    +		"Users - XS":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
    +		"Users - S":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
    +		"Users - M":      {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
    +		"Users - L":      {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
    +		"Peers - L":      {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
    +		"Groups - L":     {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
    +		"Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
    +		"Users - XL":     {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 300},
     	}
     
     	log.SetOutput(io.Discard)
     	defer log.SetOutput(os.Stderr)
     
    -	recorder := httptest.NewRecorder()
    -
     	for name, bc := range benchCasesUsers {
     		b.Run(name, func(b *testing.B) {
     			apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
     			testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys)
     
    +			recorder := httptest.NewRecorder()
     			b.ResetTimer()
     			start := time.Now()
     			for i := 0; i < b.N; i++ {
    @@ -152,26 +149,25 @@ func BenchmarkGetAllUsers(b *testing.B) {
     
     func BenchmarkDeleteUsers(b *testing.B) {
     	var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
    -		"Users - XS":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Users - S":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Users - M":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Users - L":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Peers - L":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Groups - L":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Users - XL":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    +		"Users - XS":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Users - S":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Users - M":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Users - L":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Peers - L":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Groups - L":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Users - XL":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
     	}
     
     	log.SetOutput(io.Discard)
     	defer log.SetOutput(os.Stderr)
     
    -	recorder := httptest.NewRecorder()
    -
     	for name, bc := range benchCasesUsers {
     		b.Run(name, func(b *testing.B) {
     			apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
     			testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, 1000, bc.SetupKeys)
     
    +			recorder := httptest.NewRecorder()
     			b.ResetTimer()
     			start := time.Now()
     			for i := 0; i < b.N; i++ {
    diff --git a/management/server/http/testing/testing_tools/tools.go b/management/server/http/testing/testing_tools/tools.go
    index 006d5679c..e534dac46 100644
    --- a/management/server/http/testing/testing_tools/tools.go
    +++ b/management/server/http/testing/testing_tools/tools.go
    @@ -3,6 +3,7 @@ package testing_tools
     import (
     	"bytes"
     	"context"
    +	"errors"
     	"fmt"
     	"io"
     	"net"
    @@ -13,17 +14,17 @@ import (
     	"testing"
     	"time"
     
    -	"github.com/netbirdio/netbird/management/server/util"
    +	"github.com/golang-jwt/jwt"
     	"github.com/stretchr/testify/assert"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/management/server"
     	"github.com/netbirdio/netbird/management/server/activity"
    +	"github.com/netbirdio/netbird/management/server/auth"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/groups"
     	nbhttp "github.com/netbirdio/netbird/management/server/http"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/networks"
     	"github.com/netbirdio/netbird/management/server/networks/resources"
     	"github.com/netbirdio/netbird/management/server/networks/routers"
    @@ -32,6 +33,7 @@ import (
     	"github.com/netbirdio/netbird/management/server/store"
     	"github.com/netbirdio/netbird/management/server/telemetry"
     	"github.com/netbirdio/netbird/management/server/types"
    +	"github.com/netbirdio/netbird/management/server/util"
     )
     
     const (
    @@ -115,11 +117,20 @@ func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *serve
     		t.Fatalf("Failed to create manager: %v", err)
     	}
     
    +	// @note this is required so that PAT's validate from store, but JWT's are mocked
    +	authManager := auth.NewManager(store, "", "", "", "", []string{}, false)
    +	authManagerMock := &auth.MockManager{
    +		ValidateAndParseTokenFunc:       mockValidateAndParseToken,
    +		EnsureUserAccessByJWTGroupsFunc: authManager.EnsureUserAccessByJWTGroups,
    +		MarkPATUsedFunc:                 authManager.MarkPATUsed,
    +		GetPATInfoFunc:                  authManager.GetPATInfo,
    +	}
    +
     	networksManagerMock := networks.NewManagerMock()
     	resourcesManagerMock := resources.NewManagerMock()
     	routersManagerMock := routers.NewManagerMock()
     	groupsManagerMock := groups.NewManagerMock()
    -	apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, &jwtclaims.JwtValidatorMock{}, metrics, configs.AuthCfg{}, validatorMock)
    +	apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, &server.Config{}, validatorMock)
     	if err != nil {
     		t.Fatalf("Failed to create API handler: %v", err)
     	}
    @@ -309,3 +320,25 @@ func EvaluateBenchmarkResults(b *testing.B, name string, duration time.Duration,
     		b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", name, msPerOp, maxExpected)
     	}
     }
    +
    +func mockValidateAndParseToken(_ context.Context, token string) (nbcontext.UserAuth, *jwt.Token, error) {
    +	userAuth := nbcontext.UserAuth{}
    +
    +	switch token {
    +	case "testUserId", "testAdminId", "testOwnerId", "testServiceUserId", "testServiceAdminId", "blockedUserId":
    +		userAuth.UserId = token
    +		userAuth.AccountId = "testAccountId"
    +		userAuth.Domain = "test.com"
    +		userAuth.DomainCategory = "private"
    +	case "otherUserId":
    +		userAuth.UserId = "otherUserId"
    +		userAuth.AccountId = "otherAccountId"
    +		userAuth.Domain = "other.com"
    +		userAuth.DomainCategory = "private"
    +	case "invalidToken":
    +		return userAuth, nil, errors.New("invalid token")
    +	}
    +
    +	jwtToken := jwt.New(jwt.SigningMethodHS256)
    +	return userAuth, jwtToken, nil
    +}
    diff --git a/management/server/jwtclaims/claims.go b/management/server/jwtclaims/claims.go
    deleted file mode 100644
    index 2527acbe3..000000000
    --- a/management/server/jwtclaims/claims.go
    +++ /dev/null
    @@ -1,19 +0,0 @@
    -package jwtclaims
    -
    -import (
    -	"time"
    -
    -	"github.com/golang-jwt/jwt"
    -)
    -
    -// AuthorizationClaims stores authorization information from JWTs
    -type AuthorizationClaims struct {
    -	UserId         string
    -	AccountId      string
    -	Domain         string
    -	DomainCategory string
    -	LastLogin      time.Time
    -	Invited        bool
    -
    -	Raw jwt.MapClaims
    -}
    diff --git a/management/server/jwtclaims/extractor_test.go b/management/server/jwtclaims/extractor_test.go
    deleted file mode 100644
    index eccd7c9e7..000000000
    --- a/management/server/jwtclaims/extractor_test.go
    +++ /dev/null
    @@ -1,227 +0,0 @@
    -package jwtclaims
    -
    -import (
    -	"context"
    -	"net/http"
    -	"testing"
    -	"time"
    -
    -	"github.com/golang-jwt/jwt"
    -	"github.com/stretchr/testify/require"
    -)
    -
    -func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audience string) *http.Request {
    -	t.Helper()
    -	const layout = "2006-01-02T15:04:05.999Z"
    -
    -	claimMaps := jwt.MapClaims{}
    -	if claims.UserId != "" {
    -		claimMaps[UserIDClaim] = claims.UserId
    -	}
    -	if claims.AccountId != "" {
    -		claimMaps[audience+AccountIDSuffix] = claims.AccountId
    -	}
    -	if claims.Domain != "" {
    -		claimMaps[audience+DomainIDSuffix] = claims.Domain
    -	}
    -	if claims.DomainCategory != "" {
    -		claimMaps[audience+DomainCategorySuffix] = claims.DomainCategory
    -	}
    -	if claims.LastLogin != (time.Time{}) {
    -		claimMaps[audience+LastLoginSuffix] = claims.LastLogin.Format(layout)
    -	}
    -
    -	if claims.Invited {
    -		claimMaps[audience+Invited] = true
    -	}
    -	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
    -	r, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
    -	require.NoError(t, err, "creating testing request failed")
    -	testRequest := r.WithContext(context.WithValue(r.Context(), TokenUserProperty, token)) // nolint
    -
    -	return testRequest
    -}
    -
    -func TestExtractClaimsFromRequestContext(t *testing.T) {
    -	type test struct {
    -		name                     string
    -		inputAuthorizationClaims AuthorizationClaims
    -		inputAudiance            string
    -		testingFunc              require.ComparisonAssertionFunc
    -		expectedMSG              string
    -	}
    -
    -	const layout = "2006-01-02T15:04:05.999Z"
    -	lastLogin, _ := time.Parse(layout, "2023-08-17T09:30:40.465Z")
    -
    -	testCase1 := test{
    -		name:          "All Claim Fields",
    -		inputAudiance: "https://login/",
    -		inputAuthorizationClaims: AuthorizationClaims{
    -			UserId:         "test",
    -			Domain:         "test.com",
    -			AccountId:      "testAcc",
    -			LastLogin:      lastLogin,
    -			DomainCategory: "public",
    -			Invited:        true,
    -			Raw: jwt.MapClaims{
    -				"https://login/wt_account_domain":          "test.com",
    -				"https://login/wt_account_domain_category": "public",
    -				"https://login/wt_account_id":              "testAcc",
    -				"https://login/nb_last_login":              lastLogin.Format(layout),
    -				"sub":                                      "test",
    -				"https://login/" + Invited:                 true,
    -			},
    -		},
    -		testingFunc: require.EqualValues,
    -		expectedMSG: "extracted claims should match input claims",
    -	}
    -
    -	testCase2 := test{
    -		name:          "Domain Is Empty",
    -		inputAudiance: "https://login/",
    -		inputAuthorizationClaims: AuthorizationClaims{
    -			UserId:    "test",
    -			AccountId: "testAcc",
    -			Raw: jwt.MapClaims{
    -				"https://login/wt_account_id": "testAcc",
    -				"sub":                         "test",
    -			},
    -		},
    -		testingFunc: require.EqualValues,
    -		expectedMSG: "extracted claims should match input claims",
    -	}
    -
    -	testCase3 := test{
    -		name:          "Account ID Is Empty",
    -		inputAudiance: "https://login/",
    -		inputAuthorizationClaims: AuthorizationClaims{
    -			UserId: "test",
    -			Domain: "test.com",
    -			Raw: jwt.MapClaims{
    -				"https://login/wt_account_domain": "test.com",
    -				"sub":                             "test",
    -			},
    -		},
    -		testingFunc: require.EqualValues,
    -		expectedMSG: "extracted claims should match input claims",
    -	}
    -
    -	testCase4 := test{
    -		name:          "Category Is Empty",
    -		inputAudiance: "https://login/",
    -		inputAuthorizationClaims: AuthorizationClaims{
    -			UserId:    "test",
    -			Domain:    "test.com",
    -			AccountId: "testAcc",
    -			Raw: jwt.MapClaims{
    -				"https://login/wt_account_domain": "test.com",
    -				"https://login/wt_account_id":     "testAcc",
    -				"sub":                             "test",
    -			},
    -		},
    -		testingFunc: require.EqualValues,
    -		expectedMSG: "extracted claims should match input claims",
    -	}
    -
    -	testCase5 := test{
    -		name:          "Only User ID Is set",
    -		inputAudiance: "https://login/",
    -		inputAuthorizationClaims: AuthorizationClaims{
    -			UserId: "test",
    -			Raw: jwt.MapClaims{
    -				"sub": "test",
    -			},
    -		},
    -		testingFunc: require.EqualValues,
    -		expectedMSG: "extracted claims should match input claims",
    -	}
    -
    -	for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5} {
    -		t.Run(testCase.name, func(t *testing.T) {
    -			request := newTestRequestWithJWT(t, testCase.inputAuthorizationClaims, testCase.inputAudiance)
    -
    -			extractor := NewClaimsExtractor(WithAudience(testCase.inputAudiance))
    -			extractedClaims := extractor.FromRequestContext(request)
    -
    -			testCase.testingFunc(t, testCase.inputAuthorizationClaims, extractedClaims, testCase.expectedMSG)
    -		})
    -	}
    -}
    -
    -func TestExtractClaimsSetOptions(t *testing.T) {
    -	t.Helper()
    -	type test struct {
    -		name      string
    -		extractor *ClaimsExtractor
    -		check     func(t *testing.T, c test)
    -	}
    -
    -	testCase1 := test{
    -		name:      "No custom options",
    -		extractor: NewClaimsExtractor(),
    -		check: func(t *testing.T, c test) {
    -			t.Helper()
    -			if c.extractor.authAudience != "" {
    -				t.Error("audience should be empty")
    -				return
    -			}
    -			if c.extractor.userIDClaim != UserIDClaim {
    -				t.Errorf("user id claim should be default, expected %s, got %s", UserIDClaim, c.extractor.userIDClaim)
    -				return
    -			}
    -			if c.extractor.FromRequestContext == nil {
    -				t.Error("from request context should not be nil")
    -				return
    -			}
    -		},
    -	}
    -
    -	testCase2 := test{
    -		name:      "Custom audience",
    -		extractor: NewClaimsExtractor(WithAudience("https://login/")),
    -		check: func(t *testing.T, c test) {
    -			t.Helper()
    -			if c.extractor.authAudience != "https://login/" {
    -				t.Errorf("audience expected %s, got %s", "https://login/", c.extractor.authAudience)
    -				return
    -			}
    -		},
    -	}
    -
    -	testCase3 := test{
    -		name:      "Custom user id claim",
    -		extractor: NewClaimsExtractor(WithUserIDClaim("customUserId")),
    -		check: func(t *testing.T, c test) {
    -			t.Helper()
    -			if c.extractor.userIDClaim != "customUserId" {
    -				t.Errorf("user id claim expected %s, got %s", "customUserId", c.extractor.userIDClaim)
    -				return
    -			}
    -		},
    -	}
    -
    -	testCase4 := test{
    -		name: "Custom extractor from request context",
    -		extractor: NewClaimsExtractor(
    -			WithFromRequestContext(func(r *http.Request) AuthorizationClaims {
    -				return AuthorizationClaims{
    -					UserId: "testCustomRequest",
    -				}
    -			})),
    -		check: func(t *testing.T, c test) {
    -			t.Helper()
    -			claims := c.extractor.FromRequestContext(&http.Request{})
    -			if claims.UserId != "testCustomRequest" {
    -				t.Errorf("user id claim expected %s, got %s", "testCustomRequest", claims.UserId)
    -				return
    -			}
    -		},
    -	}
    -
    -	for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4} {
    -		t.Run(testCase.name, func(t *testing.T) {
    -			testCase.check(t, testCase)
    -		})
    -	}
    -}
    diff --git a/management/server/jwtclaims/jwtValidator.go b/management/server/jwtclaims/jwtValidator.go
    deleted file mode 100644
    index 79e59e76f..000000000
    --- a/management/server/jwtclaims/jwtValidator.go
    +++ /dev/null
    @@ -1,349 +0,0 @@
    -package jwtclaims
    -
    -import (
    -	"context"
    -	"crypto/ecdsa"
    -	"crypto/elliptic"
    -	"crypto/rsa"
    -	"encoding/base64"
    -	"encoding/json"
    -	"errors"
    -	"fmt"
    -	"math/big"
    -	"net/http"
    -	"strconv"
    -	"strings"
    -	"sync"
    -	"time"
    -
    -	"github.com/golang-jwt/jwt"
    -	log "github.com/sirupsen/logrus"
    -)
    -
    -// Options is a struct for specifying configuration options for the middleware.
    -type Options struct {
    -	// The function that will return the Key to validate the JWT.
    -	// It can be either a shared secret or a public key.
    -	// Default value: nil
    -	ValidationKeyGetter jwt.Keyfunc
    -	// The name of the property in the request where the user information
    -	// from the JWT will be stored.
    -	// Default value: "user"
    -	UserProperty string
    -	// The function that will be called when there's an error validating the token
    -	// Default value:
    -	CredentialsOptional bool
    -	// A function that extracts the token from the request
    -	// Default: FromAuthHeader (i.e., from Authorization header as bearer token)
    -	Debug bool
    -	// When set, all requests with the OPTIONS method will use authentication
    -	// Default: false
    -	EnableAuthOnOptions bool
    -}
    -
    -// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation
    -type Jwks struct {
    -	Keys          []JSONWebKey `json:"keys"`
    -	expiresInTime time.Time
    -}
    -
    -// The supported elliptic curves types
    -const (
    -	// p256 represents a cryptographic elliptical curve type.
    -	p256 = "P-256"
    -
    -	// p384 represents a cryptographic elliptical curve type.
    -	p384 = "P-384"
    -
    -	// p521 represents a cryptographic elliptical curve type.
    -	p521 = "P-521"
    -)
    -
    -// JSONWebKey is a representation of a Jason Web Key
    -type JSONWebKey struct {
    -	Kty string   `json:"kty"`
    -	Kid string   `json:"kid"`
    -	Use string   `json:"use"`
    -	N   string   `json:"n"`
    -	E   string   `json:"e"`
    -	Crv string   `json:"crv"`
    -	X   string   `json:"x"`
    -	Y   string   `json:"y"`
    -	X5c []string `json:"x5c"`
    -}
    -
    -type JWTValidator interface {
    -	ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error)
    -}
    -
    -// jwtValidatorImpl struct to handle token validation and parsing
    -type jwtValidatorImpl struct {
    -	options Options
    -}
    -
    -var keyNotFound = errors.New("unable to find appropriate key")
    -
    -// NewJWTValidator constructor
    -func NewJWTValidator(ctx context.Context, issuer string, audienceList []string, keysLocation string, idpSignkeyRefreshEnabled bool) (JWTValidator, error) {
    -	keys, err := getPemKeys(ctx, keysLocation)
    -	if err != nil {
    -		return nil, err
    -	}
    -
    -	var lock sync.Mutex
    -	options := Options{
    -		ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
    -			// Verify 'aud' claim
    -			var checkAud bool
    -			for _, audience := range audienceList {
    -				checkAud = token.Claims.(jwt.MapClaims).VerifyAudience(audience, false)
    -				if checkAud {
    -					break
    -				}
    -			}
    -			if !checkAud {
    -				return token, errors.New("invalid audience")
    -			}
    -			// Verify 'issuer' claim
    -			checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(issuer, false)
    -			if !checkIss {
    -				return token, errors.New("invalid issuer")
    -			}
    -
    -			// If keys are rotated, verify the keys prior to token validation
    -			if idpSignkeyRefreshEnabled {
    -				// If the keys are invalid, retrieve new ones
    -				if !keys.stillValid() {
    -					lock.Lock()
    -					defer lock.Unlock()
    -
    -					refreshedKeys, err := getPemKeys(ctx, keysLocation)
    -					if err != nil {
    -						log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err)
    -						refreshedKeys = keys
    -					}
    -
    -					log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
    -
    -					keys = refreshedKeys
    -				}
    -			}
    -
    -			publicKey, err := getPublicKey(ctx, token, keys)
    -			if err == nil {
    -				return publicKey, nil
    -			}
    -
    -			msg := fmt.Sprintf("getPublicKey error: %s", err)
    -			if errors.Is(err, keyNotFound) && !idpSignkeyRefreshEnabled {
    -				msg = fmt.Sprintf("getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service", err)
    -			}
    -
    -			log.WithContext(ctx).Error(msg)
    -
    -			return nil, err
    -		},
    -		EnableAuthOnOptions: false,
    -	}
    -
    -	if options.UserProperty == "" {
    -		options.UserProperty = "user"
    -	}
    -
    -	return &jwtValidatorImpl{
    -		options: options,
    -	}, nil
    -}
    -
    -// ValidateAndParse validates the token and returns the parsed token
    -func (m *jwtValidatorImpl) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) {
    -	// If the token is empty...
    -	if token == "" {
    -		// Check if it was required
    -		if m.options.CredentialsOptional {
    -			log.WithContext(ctx).Debugf("no credentials found (CredentialsOptional=true)")
    -			// No error, just no token (and that is ok given that CredentialsOptional is true)
    -			return nil, nil //nolint:nilnil
    -		}
    -
    -		// If we get here, the required token is missing
    -		errorMsg := "required authorization token not found"
    -		log.WithContext(ctx).Debugf("  Error: No credentials found (CredentialsOptional=false)")
    -		return nil, errors.New(errorMsg)
    -	}
    -
    -	// Now parse the token
    -	parsedToken, err := jwt.Parse(token, m.options.ValidationKeyGetter)
    -
    -	// Check if there was an error in parsing...
    -	if err != nil {
    -		log.WithContext(ctx).Errorf("error parsing token: %v", err)
    -		return nil, fmt.Errorf("error parsing token: %w", err)
    -	}
    -
    -	// Check if the parsed token is valid...
    -	if !parsedToken.Valid {
    -		errorMsg := "token is invalid"
    -		log.WithContext(ctx).Debug(errorMsg)
    -		return nil, errors.New(errorMsg)
    -	}
    -
    -	return parsedToken, nil
    -}
    -
    -// stillValid returns true if the JSONWebKey still valid and have enough time to be used
    -func (jwks *Jwks) stillValid() bool {
    -	return !jwks.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(jwks.expiresInTime)
    -}
    -
    -func getPemKeys(ctx context.Context, keysLocation string) (*Jwks, error) {
    -	resp, err := http.Get(keysLocation)
    -	if err != nil {
    -		return nil, err
    -	}
    -	defer resp.Body.Close()
    -
    -	jwks := &Jwks{}
    -	err = json.NewDecoder(resp.Body).Decode(jwks)
    -	if err != nil {
    -		return jwks, err
    -	}
    -
    -	cacheControlHeader := resp.Header.Get("Cache-Control")
    -	expiresIn := getMaxAgeFromCacheHeader(ctx, cacheControlHeader)
    -	jwks.expiresInTime = time.Now().Add(time.Duration(expiresIn) * time.Second)
    -
    -	return jwks, err
    -}
    -
    -func getPublicKey(ctx context.Context, token *jwt.Token, jwks *Jwks) (interface{}, error) {
    -	// todo as we load the jkws when the server is starting, we should build a JKS map with the pem cert at the boot time
    -
    -	for k := range jwks.Keys {
    -		if token.Header["kid"] != jwks.Keys[k].Kid {
    -			continue
    -		}
    -
    -		if len(jwks.Keys[k].X5c) != 0 {
    -			cert := "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
    -			return jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
    -		}
    -
    -		if jwks.Keys[k].Kty == "RSA" {
    -			log.WithContext(ctx).Debugf("generating PublicKey from RSA JWK")
    -			return getPublicKeyFromRSA(jwks.Keys[k])
    -		}
    -		if jwks.Keys[k].Kty == "EC" {
    -			log.WithContext(ctx).Debugf("generating PublicKey from ECDSA JWK")
    -			return getPublicKeyFromECDSA(jwks.Keys[k])
    -		}
    -
    -		log.WithContext(ctx).Debugf("Key Type: %s not yet supported, please raise ticket!", jwks.Keys[k].Kty)
    -	}
    -
    -	return nil, keyNotFound
    -}
    -
    -func getPublicKeyFromECDSA(jwk JSONWebKey) (publicKey *ecdsa.PublicKey, err error) {
    -
    -	if jwk.X == "" || jwk.Y == "" || jwk.Crv == "" {
    -		return nil, fmt.Errorf("ecdsa key incomplete")
    -	}
    -
    -	var xCoordinate []byte
    -	if xCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.X); err != nil {
    -		return nil, err
    -	}
    -
    -	var yCoordinate []byte
    -	if yCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.Y); err != nil {
    -		return nil, err
    -	}
    -
    -	publicKey = &ecdsa.PublicKey{}
    -
    -	var curve elliptic.Curve
    -	switch jwk.Crv {
    -	case p256:
    -		curve = elliptic.P256()
    -	case p384:
    -		curve = elliptic.P384()
    -	case p521:
    -		curve = elliptic.P521()
    -	}
    -
    -	publicKey.Curve = curve
    -	publicKey.X = big.NewInt(0).SetBytes(xCoordinate)
    -	publicKey.Y = big.NewInt(0).SetBytes(yCoordinate)
    -
    -	return publicKey, nil
    -}
    -
    -func getPublicKeyFromRSA(jwk JSONWebKey) (*rsa.PublicKey, error) {
    -
    -	decodedE, err := base64.RawURLEncoding.DecodeString(jwk.E)
    -	if err != nil {
    -		return nil, err
    -	}
    -	decodedN, err := base64.RawURLEncoding.DecodeString(jwk.N)
    -	if err != nil {
    -		return nil, err
    -	}
    -
    -	var n, e big.Int
    -	e.SetBytes(decodedE)
    -	n.SetBytes(decodedN)
    -
    -	return &rsa.PublicKey{
    -		E: int(e.Int64()),
    -		N: &n,
    -	}, nil
    -}
    -
    -// getMaxAgeFromCacheHeader extracts max-age directive from the Cache-Control header
    -func getMaxAgeFromCacheHeader(ctx context.Context, cacheControl string) int {
    -	// Split into individual directives
    -	directives := strings.Split(cacheControl, ",")
    -
    -	for _, directive := range directives {
    -		directive = strings.TrimSpace(directive)
    -		if strings.HasPrefix(directive, "max-age=") {
    -			// Extract the max-age value
    -			maxAgeStr := strings.TrimPrefix(directive, "max-age=")
    -			maxAge, err := strconv.Atoi(maxAgeStr)
    -			if err != nil {
    -				log.WithContext(ctx).Debugf("error parsing max-age: %v", err)
    -				return 0
    -			}
    -
    -			return maxAge
    -		}
    -	}
    -
    -	return 0
    -}
    -
    -type JwtValidatorMock struct{}
    -
    -func (j *JwtValidatorMock) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) {
    -	claimMaps := jwt.MapClaims{}
    -
    -	switch token {
    -	case "testUserId", "testAdminId", "testOwnerId", "testServiceUserId", "testServiceAdminId", "blockedUserId":
    -		claimMaps[UserIDClaim] = token
    -		claimMaps[AccountIDSuffix] = "testAccountId"
    -		claimMaps[DomainIDSuffix] = "test.com"
    -		claimMaps[DomainCategorySuffix] = "private"
    -	case "otherUserId":
    -		claimMaps[UserIDClaim] = "otherUserId"
    -		claimMaps[AccountIDSuffix] = "otherAccountId"
    -		claimMaps[DomainIDSuffix] = "other.com"
    -		claimMaps[DomainCategorySuffix] = "private"
    -	case "invalidToken":
    -		return nil, errors.New("invalid token")
    -	}
    -
    -	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
    -	return jwtToken, nil
    -}
    -
    diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go
    index 9c2ce5ad2..4d0630f0f 100644
    --- a/management/server/management_proto_test.go
    +++ b/management/server/management_proto_test.go
    @@ -440,7 +440,7 @@ func startManagementForTest(t *testing.T, testFile string, config *Config) (*grp
     	secretsManager := NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
     
     	ephemeralMgr := NewEphemeralManager(store, accountManager)
    -	mgmtServer, err := NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, ephemeralMgr)
    +	mgmtServer, err := NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, ephemeralMgr, nil)
     	if err != nil {
     		return nil, nil, "", cleanup, err
     	}
    diff --git a/management/server/management_test.go b/management/server/management_test.go
    index 1b91b3447..fd82d8037 100644
    --- a/management/server/management_test.go
    +++ b/management/server/management_test.go
    @@ -204,6 +204,7 @@ func startServer(
     		secretsManager,
     		nil,
     		nil,
    +		nil,
     	)
     	if err != nil {
     		t.Fatalf("failed creating management server: %v", err)
    diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go
    index b2a90f156..67c23b95d 100644
    --- a/management/server/mock_server/account_mock.go
    +++ b/management/server/mock_server/account_mock.go
    @@ -13,14 +13,16 @@ import (
     	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/server"
     	"github.com/netbirdio/netbird/management/server/activity"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/idp"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/types"
     	"github.com/netbirdio/netbird/route"
     )
     
    +var _ server.AccountManager = (*MockAccountManager)(nil)
    +
     type MockAccountManager struct {
     	GetOrCreateAccountByUserFunc func(ctx context.Context, userId, domain string) (*types.Account, error)
     	GetAccountFunc               func(ctx context.Context, accountID string) (*types.Account, error)
    @@ -29,7 +31,7 @@ type MockAccountManager struct {
     	GetSetupKeyFunc                     func(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error)
     	AccountExistsFunc                   func(ctx context.Context, accountID string) (bool, error)
     	GetAccountIDByUserIdFunc            func(ctx context.Context, userId, domain string) (string, error)
    -	GetUserFunc                         func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error)
    +	GetUserFromUserAuthFunc             func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
     	ListUsersFunc                       func(ctx context.Context, accountID string) ([]*types.User, error)
     	GetPeersFunc                        func(ctx context.Context, accountID, userID string) ([]*nbpeer.Peer, error)
     	MarkPeerConnectedFunc               func(ctx context.Context, peerKey string, connected bool, realIP net.IP) error
    @@ -54,8 +56,6 @@ type MockAccountManager struct {
     	DeletePolicyFunc                    func(ctx context.Context, accountID, policyID, userID string) error
     	ListPoliciesFunc                    func(ctx context.Context, accountID, userID string) ([]*types.Policy, error)
     	GetUsersFromAccountFunc             func(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error)
    -	GetPATInfoFunc                      func(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error)
    -	MarkPATUsedFunc                     func(ctx context.Context, pat string) error
     	UpdatePeerMetaFunc                  func(ctx context.Context, peerID string, meta nbpeer.PeerSystemMeta) error
     	UpdatePeerFunc                      func(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error)
     	CreateRouteFunc                     func(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peer string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool) (*route.Route, error)
    @@ -80,8 +80,7 @@ type MockAccountManager struct {
     	DeleteNameServerGroupFunc           func(ctx context.Context, accountID, nsGroupID, userID string) error
     	ListNameServerGroupsFunc            func(ctx context.Context, accountID string, userID string) ([]*nbdns.NameServerGroup, error)
     	CreateUserFunc                      func(ctx context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error)
    -	GetAccountIDFromTokenFunc           func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
    -	CheckUserAccessByJWTGroupsFunc      func(ctx context.Context, claims jwtclaims.AuthorizationClaims) error
    +	GetAccountIDFromUserAuthFunc        func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
     	DeleteAccountFunc                   func(ctx context.Context, accountID, userID string) error
     	GetDNSDomainFunc                    func() string
     	StoreEventFunc                      func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any)
    @@ -240,14 +239,6 @@ func (am *MockAccountManager) MarkPeerConnected(ctx context.Context, peerKey str
     	return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented")
     }
     
    -// GetPATInfo mock implementation of GetPATInfo from server.AccountManager interface
    -func (am *MockAccountManager) GetPATInfo(ctx context.Context, pat string) (*types.User, *types.PersonalAccessToken, string, string, error) {
    -	if am.GetPATInfoFunc != nil {
    -		return am.GetPATInfoFunc(ctx, pat)
    -	}
    -	return nil, nil, "", "", status.Errorf(codes.Unimplemented, "method GetPATInfo is not implemented")
    -}
    -
     // DeleteAccount mock implementation of DeleteAccount from server.AccountManager interface
     func (am *MockAccountManager) DeleteAccount(ctx context.Context, accountID, userID string) error {
     	if am.DeleteAccountFunc != nil {
    @@ -256,14 +247,6 @@ func (am *MockAccountManager) DeleteAccount(ctx context.Context, accountID, user
     	return status.Errorf(codes.Unimplemented, "method DeleteAccount is not implemented")
     }
     
    -// MarkPATUsed mock implementation of MarkPATUsed from server.AccountManager interface
    -func (am *MockAccountManager) MarkPATUsed(ctx context.Context, pat string) error {
    -	if am.MarkPATUsedFunc != nil {
    -		return am.MarkPATUsedFunc(ctx, pat)
    -	}
    -	return status.Errorf(codes.Unimplemented, "method MarkPATUsed is not implemented")
    -}
    -
     // CreatePAT mock implementation of GetPAT from server.AccountManager interface
     func (am *MockAccountManager) CreatePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, name string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) {
     	if am.CreatePATFunc != nil {
    @@ -430,11 +413,11 @@ func (am *MockAccountManager) UpdatePeerMeta(ctx context.Context, peerID string,
     }
     
     // GetUser mock implementation of GetUser from server.AccountManager interface
    -func (am *MockAccountManager) GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) {
    -	if am.GetUserFunc != nil {
    -		return am.GetUserFunc(ctx, claims)
    +func (am *MockAccountManager) GetUserFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) {
    +	if am.GetUserFromUserAuthFunc != nil {
    +		return am.GetUserFromUserAuthFunc(ctx, userAuth)
     	}
    -	return nil, status.Errorf(codes.Unimplemented, "method GetUser is not implemented")
    +	return nil, status.Errorf(codes.Unimplemented, "method GetUserFromUserAuth is not implemented")
     }
     
     func (am *MockAccountManager) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) {
    @@ -614,19 +597,11 @@ func (am *MockAccountManager) CreateUser(ctx context.Context, accountID, userID
     	return nil, status.Errorf(codes.Unimplemented, "method CreateUser is not implemented")
     }
     
    -// GetAccountIDFromToken mocks GetAccountIDFromToken of the AccountManager interface
    -func (am *MockAccountManager) GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -	if am.GetAccountIDFromTokenFunc != nil {
    -		return am.GetAccountIDFromTokenFunc(ctx, claims)
    +func (am *MockAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
    +	if am.GetAccountIDFromUserAuthFunc != nil {
    +		return am.GetAccountIDFromUserAuthFunc(ctx, userAuth)
     	}
    -	return "", "", status.Errorf(codes.Unimplemented, "method GetAccountIDFromToken is not implemented")
    -}
    -
    -func (am *MockAccountManager) CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error {
    -	if am.CheckUserAccessByJWTGroupsFunc != nil {
    -		return am.CheckUserAccessByJWTGroupsFunc(ctx, claims)
    -	}
    -	return status.Errorf(codes.Unimplemented, "method CheckUserAccessByJWTGroups is not implemented")
    +	return "", "", status.Errorf(codes.Unimplemented, "method GetAccountIDFromUserAuth is not implemented")
     }
     
     // GetPeers mocks GetPeers of the AccountManager interface
    @@ -859,3 +834,7 @@ func (am *MockAccountManager) BuildUserInfosForAccount(ctx context.Context, acco
     	}
     	return nil, status.Errorf(codes.Unimplemented, "method BuildUserInfosForAccount is not implemented")
     }
    +
    +func (am *MockAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error {
    +	return status.Errorf(codes.Unimplemented, "method SyncUserJWTGroups is not implemented")
    +}
    diff --git a/management/server/user.go b/management/server/user.go
    index 6ba9b68d3..381879ae6 100644
    --- a/management/server/user.go
    +++ b/management/server/user.go
    @@ -8,16 +8,16 @@ import (
     	"time"
     
     	"github.com/google/uuid"
    +	log "github.com/sirupsen/logrus"
    +
     	"github.com/netbirdio/netbird/management/server/activity"
     	nbContext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/idp"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/store"
     	"github.com/netbirdio/netbird/management/server/types"
     	"github.com/netbirdio/netbird/management/server/util"
    -	log "github.com/sirupsen/logrus"
     )
     
     // createServiceUser creates a new service user under the given account.
    @@ -174,31 +174,26 @@ func (am *DefaultAccountManager) GetUserByID(ctx context.Context, id string) (*t
     	return am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, id)
     }
     
    -// GetUser looks up a user by provided authorization claims.
    -// It will also create an account if didn't exist for this user before.
    -func (am *DefaultAccountManager) GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) {
    -	accountID, userID, err := am.GetAccountIDFromToken(ctx, claims)
    -	if err != nil {
    -		return nil, fmt.Errorf("failed to get account with token claims %v", err)
    -	}
    -
    -	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
    +// GetUser looks up a user by provided nbContext.UserAuths.
    +// Expects account to have been created already.
    +func (am *DefaultAccountManager) GetUserFromUserAuth(ctx context.Context, userAuth nbContext.UserAuth) (*types.User, error) {
    +	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if err != nil {
     		return nil, err
     	}
     
     	// this code should be outside of the am.GetAccountIDFromToken(claims) because this method is called also by the gRPC
     	// server when user authenticates a device. And we need to separate the Dashboard login event from the Device login event.
    -	newLogin := user.LastDashboardLoginChanged(claims.LastLogin)
    +	newLogin := user.LastDashboardLoginChanged(userAuth.LastLogin)
     
    -	err = am.Store.SaveUserLastLogin(ctx, accountID, userID, claims.LastLogin)
    +	err = am.Store.SaveUserLastLogin(ctx, userAuth.AccountId, userAuth.UserId, userAuth.LastLogin)
     	if err != nil {
     		log.WithContext(ctx).Errorf("failed saving user last login: %v", err)
     	}
     
     	if newLogin {
    -		meta := map[string]any{"timestamp": claims.LastLogin}
    -		am.StoreEvent(ctx, claims.UserId, claims.UserId, accountID, activity.DashboardLogin, meta)
    +		meta := map[string]any{"timestamp": userAuth.LastLogin}
    +		am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, userAuth.AccountId, activity.DashboardLogin, meta)
     	}
     
     	return user, nil
    diff --git a/management/server/user_test.go b/management/server/user_test.go
    index 4a532c8a6..a180a761a 100644
    --- a/management/server/user_test.go
    +++ b/management/server/user_test.go
    @@ -10,6 +10,8 @@ import (
     	"github.com/eko/gocache/v3/cache"
     	cacheStore "github.com/eko/gocache/v3/store"
     	"github.com/google/go-cmp/cmp"
    +
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/util"
     	"golang.org/x/exp/maps"
     
    @@ -25,7 +27,6 @@ import (
     	"github.com/netbirdio/netbird/management/server/activity"
     	"github.com/netbirdio/netbird/management/server/idp"
     	"github.com/netbirdio/netbird/management/server/integration_reference"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     )
     
     const (
    @@ -925,11 +926,12 @@ func TestDefaultAccountManager_GetUser(t *testing.T) {
     		eventStore: &activity.InMemoryEventStore{},
     	}
     
    -	claims := jwtclaims.AuthorizationClaims{
    -		UserId: mockUserID,
    +	claims := nbcontext.UserAuth{
    +		UserId:    mockUserID,
    +		AccountId: mockAccountID,
     	}
     
    -	user, err := am.GetUser(context.Background(), claims)
    +	user, err := am.GetUserFromUserAuth(context.Background(), claims)
     	if err != nil {
     		t.Fatalf("Error when checking user role: %s", err)
     	}
    
    From 96de928cb3f87bb8454ab05e21ad5a050262cd29 Mon Sep 17 00:00:00 2001
    From: Zoltan Papp 
    Date: Fri, 21 Feb 2025 10:19:38 +0100
    Subject: [PATCH 18/23] Interface code cleaning (#3358)
    
    Code cleaning in interfaces files
    ---
     client/iface/iface_moc.go                     | 123 ------------------
     client/iface/iwginterface_windows.go          |  39 ------
     client/internal/engine.go                     |   2 +-
     client/internal/engine_test.go                | 115 +++++++++++++++-
     client/internal/iface.go                      |   8 ++
     .../iface_common.go}                          |   6 +-
     client/internal/iface_windows.go              |   6 +
     client/internal/peer/conn.go                  |   3 +-
     client/internal/peer/iface.go                 |  17 +++
     client/internal/routemanager/client.go        |  10 +-
     client/internal/routemanager/dynamic/route.go |   6 +-
     client/internal/routemanager/iface/iface.go   |   9 ++
     .../routemanager/iface/iface_common.go        |  22 ++++
     .../routemanager/iface/iface_windows.go       |   7 +
     client/internal/routemanager/manager.go       |   6 +-
     .../internal/routemanager/server_android.go   |   4 +-
     .../routemanager/server_nonandroid.go         |   6 +-
     .../routemanager/sysctl/sysctl_linux.go       |   4 +-
     .../routemanager/systemops/systemops.go       |   6 +-
     .../systemops/systemops_generic.go            |   4 +-
     20 files changed, 209 insertions(+), 194 deletions(-)
     delete mode 100644 client/iface/iface_moc.go
     delete mode 100644 client/iface/iwginterface_windows.go
     create mode 100644 client/internal/iface.go
     rename client/{iface/iwginterface.go => internal/iface_common.go} (95%)
     create mode 100644 client/internal/iface_windows.go
     create mode 100644 client/internal/peer/iface.go
     create mode 100644 client/internal/routemanager/iface/iface.go
     create mode 100644 client/internal/routemanager/iface/iface_common.go
     create mode 100644 client/internal/routemanager/iface/iface_windows.go
    
    diff --git a/client/iface/iface_moc.go b/client/iface/iface_moc.go
    deleted file mode 100644
    index f92a8cfc8..000000000
    --- a/client/iface/iface_moc.go
    +++ /dev/null
    @@ -1,123 +0,0 @@
    -package iface
    -
    -import (
    -	"net"
    -	"time"
    -
    -	wgdevice "golang.zx2c4.com/wireguard/device"
    -	"golang.zx2c4.com/wireguard/tun/netstack"
    -	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
    -
    -	"github.com/netbirdio/netbird/client/iface/bind"
    -	"github.com/netbirdio/netbird/client/iface/configurer"
    -	"github.com/netbirdio/netbird/client/iface/device"
    -	"github.com/netbirdio/netbird/client/iface/wgproxy"
    -)
    -
    -type MockWGIface struct {
    -	CreateFunc                 func() error
    -	CreateOnAndroidFunc        func(routeRange []string, ip string, domains []string) error
    -	IsUserspaceBindFunc        func() bool
    -	NameFunc                   func() string
    -	AddressFunc                func() device.WGAddress
    -	ToInterfaceFunc            func() *net.Interface
    -	UpFunc                     func() (*bind.UniversalUDPMuxDefault, error)
    -	UpdateAddrFunc             func(newAddr string) error
    -	UpdatePeerFunc             func(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
    -	RemovePeerFunc             func(peerKey string) error
    -	AddAllowedIPFunc           func(peerKey string, allowedIP string) error
    -	RemoveAllowedIPFunc        func(peerKey string, allowedIP string) error
    -	CloseFunc                  func() error
    -	SetFilterFunc              func(filter device.PacketFilter) error
    -	GetFilterFunc              func() device.PacketFilter
    -	GetDeviceFunc              func() *device.FilteredDevice
    -	GetWGDeviceFunc            func() *wgdevice.Device
    -	GetStatsFunc               func(peerKey string) (configurer.WGStats, error)
    -	GetInterfaceGUIDStringFunc func() (string, error)
    -	GetProxyFunc               func() wgproxy.Proxy
    -	GetNetFunc                 func() *netstack.Net
    -}
    -
    -func (m *MockWGIface) GetInterfaceGUIDString() (string, error) {
    -	return m.GetInterfaceGUIDStringFunc()
    -}
    -
    -func (m *MockWGIface) Create() error {
    -	return m.CreateFunc()
    -}
    -
    -func (m *MockWGIface) CreateOnAndroid(routeRange []string, ip string, domains []string) error {
    -	return m.CreateOnAndroidFunc(routeRange, ip, domains)
    -}
    -
    -func (m *MockWGIface) IsUserspaceBind() bool {
    -	return m.IsUserspaceBindFunc()
    -}
    -
    -func (m *MockWGIface) Name() string {
    -	return m.NameFunc()
    -}
    -
    -func (m *MockWGIface) Address() device.WGAddress {
    -	return m.AddressFunc()
    -}
    -
    -func (m *MockWGIface) ToInterface() *net.Interface {
    -	return m.ToInterfaceFunc()
    -}
    -
    -func (m *MockWGIface) Up() (*bind.UniversalUDPMuxDefault, error) {
    -	return m.UpFunc()
    -}
    -
    -func (m *MockWGIface) UpdateAddr(newAddr string) error {
    -	return m.UpdateAddrFunc(newAddr)
    -}
    -
    -func (m *MockWGIface) UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
    -	return m.UpdatePeerFunc(peerKey, allowedIps, keepAlive, endpoint, preSharedKey)
    -}
    -
    -func (m *MockWGIface) RemovePeer(peerKey string) error {
    -	return m.RemovePeerFunc(peerKey)
    -}
    -
    -func (m *MockWGIface) AddAllowedIP(peerKey string, allowedIP string) error {
    -	return m.AddAllowedIPFunc(peerKey, allowedIP)
    -}
    -
    -func (m *MockWGIface) RemoveAllowedIP(peerKey string, allowedIP string) error {
    -	return m.RemoveAllowedIPFunc(peerKey, allowedIP)
    -}
    -
    -func (m *MockWGIface) Close() error {
    -	return m.CloseFunc()
    -}
    -
    -func (m *MockWGIface) SetFilter(filter device.PacketFilter) error {
    -	return m.SetFilterFunc(filter)
    -}
    -
    -func (m *MockWGIface) GetFilter() device.PacketFilter {
    -	return m.GetFilterFunc()
    -}
    -
    -func (m *MockWGIface) GetDevice() *device.FilteredDevice {
    -	return m.GetDeviceFunc()
    -}
    -
    -func (m *MockWGIface) GetWGDevice() *wgdevice.Device {
    -	return m.GetWGDeviceFunc()
    -}
    -
    -func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) {
    -	return m.GetStatsFunc(peerKey)
    -}
    -
    -func (m *MockWGIface) GetProxy() wgproxy.Proxy {
    -	return m.GetProxyFunc()
    -}
    -
    -func (m *MockWGIface) GetNet() *netstack.Net {
    -	return m.GetNetFunc()
    -}
    diff --git a/client/iface/iwginterface_windows.go b/client/iface/iwginterface_windows.go
    deleted file mode 100644
    index cac096b54..000000000
    --- a/client/iface/iwginterface_windows.go
    +++ /dev/null
    @@ -1,39 +0,0 @@
    -package iface
    -
    -import (
    -	"net"
    -	"time"
    -
    -	wgdevice "golang.zx2c4.com/wireguard/device"
    -	"golang.zx2c4.com/wireguard/tun/netstack"
    -	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
    -
    -	"github.com/netbirdio/netbird/client/iface/bind"
    -	"github.com/netbirdio/netbird/client/iface/configurer"
    -	"github.com/netbirdio/netbird/client/iface/device"
    -	"github.com/netbirdio/netbird/client/iface/wgproxy"
    -)
    -
    -type IWGIface interface {
    -	Create() error
    -	CreateOnAndroid(routeRange []string, ip string, domains []string) error
    -	IsUserspaceBind() bool
    -	Name() string
    -	Address() device.WGAddress
    -	ToInterface() *net.Interface
    -	Up() (*bind.UniversalUDPMuxDefault, error)
    -	UpdateAddr(newAddr string) error
    -	GetProxy() wgproxy.Proxy
    -	UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
    -	RemovePeer(peerKey string) error
    -	AddAllowedIP(peerKey string, allowedIP string) error
    -	RemoveAllowedIP(peerKey string, allowedIP string) error
    -	Close() error
    -	SetFilter(filter device.PacketFilter) error
    -	GetFilter() device.PacketFilter
    -	GetDevice() *device.FilteredDevice
    -	GetWGDevice() *wgdevice.Device
    -	GetStats(peerKey string) (configurer.WGStats, error)
    -	GetInterfaceGUIDString() (string, error)
    -	GetNet() *netstack.Net
    -}
    diff --git a/client/internal/engine.go b/client/internal/engine.go
    index d590c0db6..90f70a827 100644
    --- a/client/internal/engine.go
    +++ b/client/internal/engine.go
    @@ -154,7 +154,7 @@ type Engine struct {
     	ctx    context.Context
     	cancel context.CancelFunc
     
    -	wgInterface iface.IWGIface
    +	wgInterface WGIface
     
     	udpMux *bind.UniversalUDPMuxDefault
     
    diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go
    index e32e262b9..2b1d3b098 100644
    --- a/client/internal/engine_test.go
    +++ b/client/internal/engine_test.go
    @@ -23,10 +23,11 @@ import (
     	"google.golang.org/grpc/keepalive"
     
     	"github.com/netbirdio/management-integrations/integrations"
    -
     	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/iface/bind"
    +	"github.com/netbirdio/netbird/client/iface/configurer"
     	"github.com/netbirdio/netbird/client/iface/device"
    +	"github.com/netbirdio/netbird/client/iface/wgproxy"
     	"github.com/netbirdio/netbird/client/internal/dns"
     	"github.com/netbirdio/netbird/client/internal/peer"
     	"github.com/netbirdio/netbird/client/internal/peer/guard"
    @@ -48,6 +49,8 @@ import (
     	"github.com/netbirdio/netbird/signal/proto"
     	signalServer "github.com/netbirdio/netbird/signal/server"
     	"github.com/netbirdio/netbird/util"
    +	wgdevice "golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     )
     
     var (
    @@ -64,6 +67,114 @@ var (
     	}
     )
     
    +type MockWGIface struct {
    +	CreateFunc                 func() error
    +	CreateOnAndroidFunc        func(routeRange []string, ip string, domains []string) error
    +	IsUserspaceBindFunc        func() bool
    +	NameFunc                   func() string
    +	AddressFunc                func() device.WGAddress
    +	ToInterfaceFunc            func() *net.Interface
    +	UpFunc                     func() (*bind.UniversalUDPMuxDefault, error)
    +	UpdateAddrFunc             func(newAddr string) error
    +	UpdatePeerFunc             func(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
    +	RemovePeerFunc             func(peerKey string) error
    +	AddAllowedIPFunc           func(peerKey string, allowedIP string) error
    +	RemoveAllowedIPFunc        func(peerKey string, allowedIP string) error
    +	CloseFunc                  func() error
    +	SetFilterFunc              func(filter device.PacketFilter) error
    +	GetFilterFunc              func() device.PacketFilter
    +	GetDeviceFunc              func() *device.FilteredDevice
    +	GetWGDeviceFunc            func() *wgdevice.Device
    +	GetStatsFunc               func(peerKey string) (configurer.WGStats, error)
    +	GetInterfaceGUIDStringFunc func() (string, error)
    +	GetProxyFunc               func() wgproxy.Proxy
    +	GetNetFunc                 func() *netstack.Net
    +}
    +
    +func (m *MockWGIface) GetInterfaceGUIDString() (string, error) {
    +	return m.GetInterfaceGUIDStringFunc()
    +}
    +
    +func (m *MockWGIface) Create() error {
    +	return m.CreateFunc()
    +}
    +
    +func (m *MockWGIface) CreateOnAndroid(routeRange []string, ip string, domains []string) error {
    +	return m.CreateOnAndroidFunc(routeRange, ip, domains)
    +}
    +
    +func (m *MockWGIface) IsUserspaceBind() bool {
    +	return m.IsUserspaceBindFunc()
    +}
    +
    +func (m *MockWGIface) Name() string {
    +	return m.NameFunc()
    +}
    +
    +func (m *MockWGIface) Address() device.WGAddress {
    +	return m.AddressFunc()
    +}
    +
    +func (m *MockWGIface) ToInterface() *net.Interface {
    +	return m.ToInterfaceFunc()
    +}
    +
    +func (m *MockWGIface) Up() (*bind.UniversalUDPMuxDefault, error) {
    +	return m.UpFunc()
    +}
    +
    +func (m *MockWGIface) UpdateAddr(newAddr string) error {
    +	return m.UpdateAddrFunc(newAddr)
    +}
    +
    +func (m *MockWGIface) UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
    +	return m.UpdatePeerFunc(peerKey, allowedIps, keepAlive, endpoint, preSharedKey)
    +}
    +
    +func (m *MockWGIface) RemovePeer(peerKey string) error {
    +	return m.RemovePeerFunc(peerKey)
    +}
    +
    +func (m *MockWGIface) AddAllowedIP(peerKey string, allowedIP string) error {
    +	return m.AddAllowedIPFunc(peerKey, allowedIP)
    +}
    +
    +func (m *MockWGIface) RemoveAllowedIP(peerKey string, allowedIP string) error {
    +	return m.RemoveAllowedIPFunc(peerKey, allowedIP)
    +}
    +
    +func (m *MockWGIface) Close() error {
    +	return m.CloseFunc()
    +}
    +
    +func (m *MockWGIface) SetFilter(filter device.PacketFilter) error {
    +	return m.SetFilterFunc(filter)
    +}
    +
    +func (m *MockWGIface) GetFilter() device.PacketFilter {
    +	return m.GetFilterFunc()
    +}
    +
    +func (m *MockWGIface) GetDevice() *device.FilteredDevice {
    +	return m.GetDeviceFunc()
    +}
    +
    +func (m *MockWGIface) GetWGDevice() *wgdevice.Device {
    +	return m.GetWGDeviceFunc()
    +}
    +
    +func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) {
    +	return m.GetStatsFunc(peerKey)
    +}
    +
    +func (m *MockWGIface) GetProxy() wgproxy.Proxy {
    +	return m.GetProxyFunc()
    +}
    +
    +func (m *MockWGIface) GetNet() *netstack.Net {
    +	return m.GetNetFunc()
    +}
    +
     func TestMain(m *testing.M) {
     	_ = util.InitLog("debug", "console")
     	code := m.Run()
    @@ -245,7 +356,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
     		peer.NewRecorder("https://mgm"),
     		nil)
     
    -	wgIface := &iface.MockWGIface{
    +	wgIface := &MockWGIface{
     		NameFunc: func() string { return "utun102" },
     		RemovePeerFunc: func(peerKey string) error {
     			return nil
    diff --git a/client/internal/iface.go b/client/internal/iface.go
    new file mode 100644
    index 000000000..bd0069c19
    --- /dev/null
    +++ b/client/internal/iface.go
    @@ -0,0 +1,8 @@
    +//go:build !windows
    +// +build !windows
    +
    +package internal
    +
    +type WGIface interface {
    +	wgIfaceBase
    +}
    diff --git a/client/iface/iwginterface.go b/client/internal/iface_common.go
    similarity index 95%
    rename from client/iface/iwginterface.go
    rename to client/internal/iface_common.go
    index 2b919ac9e..a66342707 100644
    --- a/client/iface/iwginterface.go
    +++ b/client/internal/iface_common.go
    @@ -1,6 +1,4 @@
    -//go:build !windows
    -
    -package iface
    +package internal
     
     import (
     	"net"
    @@ -16,7 +14,7 @@ import (
     	"github.com/netbirdio/netbird/client/iface/wgproxy"
     )
     
    -type IWGIface interface {
    +type wgIfaceBase interface {
     	Create() error
     	CreateOnAndroid(routeRange []string, ip string, domains []string) error
     	IsUserspaceBind() bool
    diff --git a/client/internal/iface_windows.go b/client/internal/iface_windows.go
    new file mode 100644
    index 000000000..113217815
    --- /dev/null
    +++ b/client/internal/iface_windows.go
    @@ -0,0 +1,6 @@
    +package internal
    +
    +type WGIface interface {
    +	wgIfaceBase
    +	GetInterfaceGUIDString() (string, error)
    +}
    diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go
    index 8bbea6a2b..0337960bb 100644
    --- a/client/internal/peer/conn.go
    +++ b/client/internal/peer/conn.go
    @@ -15,7 +15,6 @@ import (
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/iface/configurer"
     	"github.com/netbirdio/netbird/client/iface/wgproxy"
     	"github.com/netbirdio/netbird/client/internal/peer/guard"
    @@ -56,7 +55,7 @@ const (
     type WgConfig struct {
     	WgListenPort int
     	RemoteKey    string
    -	WgInterface  iface.IWGIface
    +	WgInterface  WGIface
     	AllowedIps   string
     	PreSharedKey *wgtypes.Key
     }
    diff --git a/client/internal/peer/iface.go b/client/internal/peer/iface.go
    new file mode 100644
    index 000000000..ae6b3bd0a
    --- /dev/null
    +++ b/client/internal/peer/iface.go
    @@ -0,0 +1,17 @@
    +package peer
    +
    +import (
    +	"net"
    +	"time"
    +
    +	"github.com/netbirdio/netbird/client/iface/configurer"
    +	"github.com/netbirdio/netbird/client/iface/wgproxy"
    +	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
    +)
    +
    +type WGIface interface {
    +	UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
    +	RemovePeer(peerKey string) error
    +	GetStats(peerKey string) (configurer.WGStats, error)
    +	GetProxy() wgproxy.Proxy
    +}
    diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go
    index 3238dd831..24a7ef467 100644
    --- a/client/internal/routemanager/client.go
    +++ b/client/internal/routemanager/client.go
    @@ -4,19 +4,19 @@ import (
     	"context"
     	"fmt"
     	"reflect"
    -	runtime "runtime"
    +	"runtime"
     	"time"
     
     	"github.com/hashicorp/go-multierror"
     	log "github.com/sirupsen/logrus"
     
     	nberrors "github.com/netbirdio/netbird/client/errors"
    -	"github.com/netbirdio/netbird/client/iface"
     	nbdns "github.com/netbirdio/netbird/client/internal/dns"
     	"github.com/netbirdio/netbird/client/internal/peer"
     	"github.com/netbirdio/netbird/client/internal/peerstore"
     	"github.com/netbirdio/netbird/client/internal/routemanager/dnsinterceptor"
     	"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     	"github.com/netbirdio/netbird/client/internal/routemanager/static"
     	"github.com/netbirdio/netbird/client/proto"
    @@ -62,7 +62,7 @@ type clientNetwork struct {
     	ctx                 context.Context
     	cancel              context.CancelFunc
     	statusRecorder      *peer.Status
    -	wgInterface         iface.IWGIface
    +	wgInterface         iface.WGIface
     	routes              map[route.ID]*route.Route
     	routeUpdate         chan routesUpdate
     	peerStateUpdate     chan struct{}
    @@ -75,7 +75,7 @@ type clientNetwork struct {
     func newClientNetworkWatcher(
     	ctx context.Context,
     	dnsRouteInterval time.Duration,
    -	wgInterface iface.IWGIface,
    +	wgInterface iface.WGIface,
     	statusRecorder *peer.Status,
     	rt *route.Route,
     	routeRefCounter *refcounter.RouteRefCounter,
    @@ -468,7 +468,7 @@ func handlerFromRoute(
     	allowedIPsRefCounter *refcounter.AllowedIPsRefCounter,
     	dnsRouterInteval time.Duration,
     	statusRecorder *peer.Status,
    -	wgInterface iface.IWGIface,
    +	wgInterface iface.WGIface,
     	dnsServer nbdns.Server,
     	peerStore *peerstore.Store,
     	useNewDNSRoute bool,
    diff --git a/client/internal/routemanager/dynamic/route.go b/client/internal/routemanager/dynamic/route.go
    index a0fff7713..5ef18a47e 100644
    --- a/client/internal/routemanager/dynamic/route.go
    +++ b/client/internal/routemanager/dynamic/route.go
    @@ -13,8 +13,8 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	nberrors "github.com/netbirdio/netbird/client/errors"
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/internal/peer"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     	"github.com/netbirdio/netbird/client/internal/routemanager/util"
     	"github.com/netbirdio/netbird/management/domain"
    @@ -48,7 +48,7 @@ type Route struct {
     	currentPeerKey       string
     	cancel               context.CancelFunc
     	statusRecorder       *peer.Status
    -	wgInterface          iface.IWGIface
    +	wgInterface          iface.WGIface
     	resolverAddr         string
     }
     
    @@ -58,7 +58,7 @@ func NewRoute(
     	allowedIPsRefCounter *refcounter.AllowedIPsRefCounter,
     	interval time.Duration,
     	statusRecorder *peer.Status,
    -	wgInterface iface.IWGIface,
    +	wgInterface iface.WGIface,
     	resolverAddr string,
     ) *Route {
     	return &Route{
    diff --git a/client/internal/routemanager/iface/iface.go b/client/internal/routemanager/iface/iface.go
    new file mode 100644
    index 000000000..57dbec03d
    --- /dev/null
    +++ b/client/internal/routemanager/iface/iface.go
    @@ -0,0 +1,9 @@
    +//go:build !windows
    +// +build !windows
    +
    +package iface
    +
    +// WGIface defines subset methods of interface required for router
    +type WGIface interface {
    +	wgIfaceBase
    +}
    diff --git a/client/internal/routemanager/iface/iface_common.go b/client/internal/routemanager/iface/iface_common.go
    new file mode 100644
    index 000000000..8b2dc9714
    --- /dev/null
    +++ b/client/internal/routemanager/iface/iface_common.go
    @@ -0,0 +1,22 @@
    +package iface
    +
    +import (
    +	"net"
    +
    +	"github.com/netbirdio/netbird/client/iface"
    +	"github.com/netbirdio/netbird/client/iface/configurer"
    +	"github.com/netbirdio/netbird/client/iface/device"
    +)
    +
    +type wgIfaceBase interface {
    +	AddAllowedIP(peerKey string, allowedIP string) error
    +	RemoveAllowedIP(peerKey string, allowedIP string) error
    +
    +	Name() string
    +	Address() iface.WGAddress
    +	ToInterface() *net.Interface
    +	IsUserspaceBind() bool
    +	GetFilter() device.PacketFilter
    +	GetDevice() *device.FilteredDevice
    +	GetStats(peerKey string) (configurer.WGStats, error)
    +}
    diff --git a/client/internal/routemanager/iface/iface_windows.go b/client/internal/routemanager/iface/iface_windows.go
    new file mode 100644
    index 000000000..7ab7e239c
    --- /dev/null
    +++ b/client/internal/routemanager/iface/iface_windows.go
    @@ -0,0 +1,7 @@
    +package iface
    +
    +// WGIface defines subset methods of interface required for router
    +type WGIface interface {
    +	wgIfaceBase
    +	GetInterfaceGUIDString() (string, error)
    +}
    diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go
    index 52de0948b..ae0d1d220 100644
    --- a/client/internal/routemanager/manager.go
    +++ b/client/internal/routemanager/manager.go
    @@ -15,13 +15,13 @@ import (
     	"golang.org/x/exp/maps"
     
     	firewall "github.com/netbirdio/netbird/client/firewall/manager"
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/iface/configurer"
     	"github.com/netbirdio/netbird/client/iface/netstack"
     	"github.com/netbirdio/netbird/client/internal/dns"
     	"github.com/netbirdio/netbird/client/internal/listener"
     	"github.com/netbirdio/netbird/client/internal/peer"
     	"github.com/netbirdio/netbird/client/internal/peerstore"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/notifier"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     	"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
    @@ -52,7 +52,7 @@ type ManagerConfig struct {
     	Context             context.Context
     	PublicKey           string
     	DNSRouteInterval    time.Duration
    -	WGInterface         iface.IWGIface
    +	WGInterface         iface.WGIface
     	StatusRecorder      *peer.Status
     	RelayManager        *relayClient.Manager
     	InitialRoutes       []*route.Route
    @@ -74,7 +74,7 @@ type DefaultManager struct {
     	sysOps               *systemops.SysOps
     	statusRecorder       *peer.Status
     	relayMgr             *relayClient.Manager
    -	wgInterface          iface.IWGIface
    +	wgInterface          iface.WGIface
     	pubKey               string
     	notifier             *notifier.Notifier
     	routeRefCounter      *refcounter.RouteRefCounter
    diff --git a/client/internal/routemanager/server_android.go b/client/internal/routemanager/server_android.go
    index e9cfa0826..48bb0380d 100644
    --- a/client/internal/routemanager/server_android.go
    +++ b/client/internal/routemanager/server_android.go
    @@ -7,8 +7,8 @@ import (
     	"fmt"
     
     	firewall "github.com/netbirdio/netbird/client/firewall/manager"
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/internal/peer"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/route"
     )
     
    @@ -22,6 +22,6 @@ func (r serverRouter) updateRoutes(map[route.ID]*route.Route) error {
     	return nil
     }
     
    -func newServerRouter(context.Context, iface.IWGIface, firewall.Manager, *peer.Status) (*serverRouter, error) {
    +func newServerRouter(context.Context, iface.WGIface, firewall.Manager, *peer.Status) (*serverRouter, error) {
     	return nil, fmt.Errorf("server route not supported on this os")
     }
    diff --git a/client/internal/routemanager/server_nonandroid.go b/client/internal/routemanager/server_nonandroid.go
    index 4690e3f0e..c9bbe10a6 100644
    --- a/client/internal/routemanager/server_nonandroid.go
    +++ b/client/internal/routemanager/server_nonandroid.go
    @@ -11,8 +11,8 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	firewall "github.com/netbirdio/netbird/client/firewall/manager"
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/internal/peer"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
     	"github.com/netbirdio/netbird/route"
     )
    @@ -22,11 +22,11 @@ type serverRouter struct {
     	ctx            context.Context
     	routes         map[route.ID]*route.Route
     	firewall       firewall.Manager
    -	wgInterface    iface.IWGIface
    +	wgInterface    iface.WGIface
     	statusRecorder *peer.Status
     }
     
    -func newServerRouter(ctx context.Context, wgInterface iface.IWGIface, firewall firewall.Manager, statusRecorder *peer.Status) (*serverRouter, error) {
    +func newServerRouter(ctx context.Context, wgInterface iface.WGIface, firewall firewall.Manager, statusRecorder *peer.Status) (*serverRouter, error) {
     	return &serverRouter{
     		ctx:            ctx,
     		routes:         make(map[route.ID]*route.Route),
    diff --git a/client/internal/routemanager/sysctl/sysctl_linux.go b/client/internal/routemanager/sysctl/sysctl_linux.go
    index bb620ee68..ea63f02fc 100644
    --- a/client/internal/routemanager/sysctl/sysctl_linux.go
    +++ b/client/internal/routemanager/sysctl/sysctl_linux.go
    @@ -13,7 +13,7 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	nberrors "github.com/netbirdio/netbird/client/errors"
    -	"github.com/netbirdio/netbird/client/iface"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     )
     
     const (
    @@ -23,7 +23,7 @@ const (
     )
     
     // Setup configures sysctl settings for RP filtering and source validation.
    -func Setup(wgIface iface.IWGIface) (map[string]int, error) {
    +func Setup(wgIface iface.WGIface) (map[string]int, error) {
     	keys := map[string]int{}
     	var result *multierror.Error
     
    diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go
    index d1cb83bfb..5c117b94d 100644
    --- a/client/internal/routemanager/systemops/systemops.go
    +++ b/client/internal/routemanager/systemops/systemops.go
    @@ -5,7 +5,7 @@ import (
     	"net/netip"
     	"sync"
     
    -	"github.com/netbirdio/netbird/client/iface"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/notifier"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     )
    @@ -19,7 +19,7 @@ type ExclusionCounter = refcounter.Counter[netip.Prefix, struct{}, Nexthop]
     
     type SysOps struct {
     	refCounter  *ExclusionCounter
    -	wgInterface iface.IWGIface
    +	wgInterface iface.WGIface
     	// prefixes is tracking all the current added prefixes im memory
     	// (this is used in iOS as all route updates require a full table update)
     	//nolint
    @@ -30,7 +30,7 @@ type SysOps struct {
     	notifier *notifier.Notifier
     }
     
    -func NewSysOps(wgInterface iface.IWGIface, notifier *notifier.Notifier) *SysOps {
    +func NewSysOps(wgInterface iface.WGIface, notifier *notifier.Notifier) *SysOps {
     	return &SysOps{
     		wgInterface: wgInterface,
     		notifier:    notifier,
    diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go
    index 31b7f3ac2..eaef01815 100644
    --- a/client/internal/routemanager/systemops/systemops_generic.go
    +++ b/client/internal/routemanager/systemops/systemops_generic.go
    @@ -16,8 +16,8 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	nberrors "github.com/netbirdio/netbird/client/errors"
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/iface/netstack"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     	"github.com/netbirdio/netbird/client/internal/routemanager/util"
     	"github.com/netbirdio/netbird/client/internal/routemanager/vars"
    @@ -149,7 +149,7 @@ func (r *SysOps) addRouteForCurrentDefaultGateway(prefix netip.Prefix) error {
     
     // addRouteToNonVPNIntf adds a new route to the routing table for the given prefix and returns the next hop and interface.
     // If the next hop or interface is pointing to the VPN interface, it will return the initial values.
    -func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf iface.IWGIface, initialNextHop Nexthop) (Nexthop, error) {
    +func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf iface.WGIface, initialNextHop Nexthop) (Nexthop, error) {
     	addr := prefix.Addr()
     	switch {
     	case addr.IsLoopback(),
    
    From a0b48f971c47025d813e5e567f659344daf9769c Mon Sep 17 00:00:00 2001
    From: Misha Bragin 
    Date: Fri, 21 Feb 2025 11:13:02 +0100
    Subject: [PATCH 19/23] Add K8s webinar to Readme
    
    ---
     README.md | 5 +++++
     1 file changed, 5 insertions(+)
    
    diff --git a/README.md b/README.md
    index 0537710e9..7cee2f8dc 100644
    --- a/README.md
    +++ b/README.md
    @@ -1,4 +1,9 @@
     
    + + Webinar: How to Achieve Zero Trust Access to Kubernetes — Effortlessly + +
    +

    From a854660402f68400ee1e0ab618e151c9bc7e67d6 Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Fri, 21 Feb 2025 03:02:50 -0800 Subject: [PATCH 20/23] [client, signal, management] Update google.golang.org/api to latest (#3288) * [misc] Add vendor/ to .gitignore Ignore the vendor/ tree created if someone runs "go mod vendor" Signed-off-by: Christian Stewart * [client, signal, management] Update google.golang.org/protobuf to latest Updating protobuf runtime library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update google.golang.org/grpc to latest Updating grpc library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update golang.org/x/net to latest Updating x/net library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update golang.org/x/oauth2 to latest Updating x/oauth2 library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update github.com/stretchr/testify to latest Updating testify library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update opentelemetry to latest Updating otel library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update golang.org/x/time to latest Updating x/time library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [management] Update google.golang.org/api to latest Updating google.golang.org/api library to fix indirect dependency issues with older versions of OpenTelemetry. See: #3240 Signed-off-by: Christian Stewart --------- Signed-off-by: Christian Stewart --- .gitignore | 1 + go.mod | 48 ++++++++++++------------ go.sum | 107 ++++++++++++++++++++++++++--------------------------- 3 files changed, 77 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index d0b4f82dd..abb728b19 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ infrastructure_files/setup.env infrastructure_files/setup-*.env .vscode .DS_Store +vendor/ diff --git a/go.mod b/go.mod index 25e5dd1d2..3d71e8eb1 100644 --- a/go.mod +++ b/go.mod @@ -24,8 +24,8 @@ require ( golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/windows v0.5.3 - google.golang.org/grpc v1.64.1 - google.golang.org/protobuf v1.34.2 + google.golang.org/grpc v1.70.0 + google.golang.org/protobuf v1.36.4 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) @@ -76,27 +76,27 @@ require ( github.com/shirou/gopsutil/v3 v3.24.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.31.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 github.com/things-go/go-socks5 v0.0.4 github.com/yusufpapurcu/wmi v1.2.4 github.com/zcalusic/sysinfo v1.1.3 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 - go.opentelemetry.io/otel v1.26.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 + go.opentelemetry.io/otel v1.34.0 go.opentelemetry.io/otel/exporters/prometheus v0.48.0 - go.opentelemetry.io/otel/metric v1.26.0 - go.opentelemetry.io/otel/sdk/metric v1.26.0 + go.opentelemetry.io/otel/metric v1.34.0 + go.opentelemetry.io/otel/sdk/metric v1.32.0 go.uber.org/zap v1.27.0 goauthentik.io/api/v3 v3.2023051.3 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a - golang.org/x/net v0.33.0 - golang.org/x/oauth2 v0.19.0 + golang.org/x/net v0.34.0 + golang.org/x/oauth2 v0.26.0 golang.org/x/sync v0.10.0 golang.org/x/term v0.28.0 - google.golang.org/api v0.177.0 + google.golang.org/api v0.220.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.5.7 @@ -106,9 +106,9 @@ require ( ) require ( - cloud.google.com/go/auth v0.3.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/auth v0.14.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect dario.cat/mergo v1.0.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect @@ -151,7 +151,7 @@ require ( github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect @@ -160,12 +160,11 @@ require ( github.com/go-text/render v0.2.0 // indirect github.com/go-text/typesetting v0.2.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -221,20 +220,19 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect github.com/yuin/goldmark v1.7.1 // indirect github.com/zeebo/blake3 v0.2.3 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect - go.opentelemetry.io/otel/sdk v1.26.0 // indirect - go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/image v0.18.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/time v0.10.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect diff --git a/go.sum b/go.sum index 4057517d3..36bca22d3 100644 --- a/go.sum +++ b/go.sum @@ -18,10 +18,10 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= +cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -29,8 +29,8 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= @@ -225,8 +225,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -263,14 +263,12 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= -github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM= +github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -345,18 +343,18 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gopacket/gopacket v1.1.1 h1:zbx9F9d6A7sWNkFKrvMBZTfGgxFoY4NgUudFVVHMfcw= github.com/gopacket/gopacket v1.1.1/go.mod h1:HavMeONEl7W9036of9LbSWoonqhH7HA1+ZRO+rMIvFs= @@ -617,8 +615,8 @@ github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KW github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= @@ -683,11 +681,11 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U= github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI= @@ -739,28 +737,28 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= -go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/prometheus v0.48.0 h1:sBQe3VNGUjY9IKWQC6z2lNqa5iGbDSxhs60ABwK4y0s= go.opentelemetry.io/otel/exporters/prometheus v0.48.0/go.mod h1:DtrbMzoZWwQHyrQmCfLam5DZbnmorsGbOtTbYHycU5o= -go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= -go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= -go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= -go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= -go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= -go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -885,8 +883,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -900,8 +898,8 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1019,8 +1017,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1114,8 +1112,8 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk= -google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= +google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns= +google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1164,10 +1162,11 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434 h1:OpXbo8JnN8+jZGPrL4SSfaDjSCjupr8lXyBAbexEm/U= -google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1188,8 +1187,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1204,8 +1203,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 6554026a82bf796161aa0df243da6d5cf1be6b0d Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 21 Feb 2025 12:04:26 +0100 Subject: [PATCH 21/23] [client] fix client/Dockerfile to reduce vulnerabilities (#3359) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-ALPINE321-MUSL-8720634 - https://snyk.io/vuln/SNYK-ALPINE321-MUSL-8720634 - https://snyk.io/vuln/SNYK-ALPINE321-OPENSSL-8690014 - https://snyk.io/vuln/SNYK-ALPINE321-OPENSSL-8690014 - https://snyk.io/vuln/SNYK-ALPINE321-OPENSSL-8710358 Co-authored-by: snyk-bot --- client/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/Dockerfile b/client/Dockerfile index 2f5ff14ae..35c1d04c2 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21.0 +FROM alpine:3.21.3 RUN apk add --no-cache ca-certificates iptables ip6tables ENV NB_FOREGROUND_MODE=true ENTRYPOINT [ "/usr/local/bin/netbird","up"] From 5134e3a06adf50700165984a6a4a0f67b7789a21 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:52:04 +0100 Subject: [PATCH 22/23] [client] Add reverse dns zone (#3217) --- client/internal/dns.go | 111 +++++++++++++++++++++++++++++ client/internal/dns/host.go | 8 ++- client/internal/dns/server.go | 10 +-- client/internal/dns/server_test.go | 8 ++- client/internal/engine.go | 11 ++- client/internal/engine_test.go | 15 ++++ 6 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 client/internal/dns.go diff --git a/client/internal/dns.go b/client/internal/dns.go new file mode 100644 index 000000000..8a73f50f2 --- /dev/null +++ b/client/internal/dns.go @@ -0,0 +1,111 @@ +package internal + +import ( + "fmt" + "net" + "slices" + "strings" + + "github.com/miekg/dns" + log "github.com/sirupsen/logrus" + + nbdns "github.com/netbirdio/netbird/dns" +) + +func createPTRRecord(aRecord nbdns.SimpleRecord, ipNet *net.IPNet) (nbdns.SimpleRecord, bool) { + ip := net.ParseIP(aRecord.RData) + if ip == nil || ip.To4() == nil { + return nbdns.SimpleRecord{}, false + } + + if !ipNet.Contains(ip) { + return nbdns.SimpleRecord{}, false + } + + ipOctets := strings.Split(ip.String(), ".") + slices.Reverse(ipOctets) + rdnsName := dns.Fqdn(strings.Join(ipOctets, ".") + ".in-addr.arpa") + + return nbdns.SimpleRecord{ + Name: rdnsName, + Type: int(dns.TypePTR), + Class: aRecord.Class, + TTL: aRecord.TTL, + RData: dns.Fqdn(aRecord.Name), + }, true +} + +// generateReverseZoneName creates the reverse DNS zone name for a given network +func generateReverseZoneName(ipNet *net.IPNet) (string, error) { + networkIP := ipNet.IP.Mask(ipNet.Mask) + maskOnes, _ := ipNet.Mask.Size() + + // round up to nearest byte + octetsToUse := (maskOnes + 7) / 8 + + octets := strings.Split(networkIP.String(), ".") + if octetsToUse > len(octets) { + return "", fmt.Errorf("invalid network mask size for reverse DNS: %d", maskOnes) + } + + reverseOctets := make([]string, octetsToUse) + for i := 0; i < octetsToUse; i++ { + reverseOctets[octetsToUse-1-i] = octets[i] + } + + return dns.Fqdn(strings.Join(reverseOctets, ".") + ".in-addr.arpa"), nil +} + +// zoneExists checks if a zone with the given name already exists in the configuration +func zoneExists(config *nbdns.Config, zoneName string) bool { + for _, zone := range config.CustomZones { + if zone.Domain == zoneName { + log.Debugf("reverse DNS zone %s already exists", zoneName) + return true + } + } + return false +} + +// collectPTRRecords gathers all PTR records for the given network from A records +func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRecord { + var records []nbdns.SimpleRecord + + for _, zone := range config.CustomZones { + for _, record := range zone.Records { + if record.Type != int(dns.TypeA) { + continue + } + + if ptrRecord, ok := createPTRRecord(record, ipNet); ok { + records = append(records, ptrRecord) + } + } + } + + return records +} + +// addReverseZone adds a reverse DNS zone to the configuration for the given network +func addReverseZone(config *nbdns.Config, ipNet *net.IPNet) { + zoneName, err := generateReverseZoneName(ipNet) + if err != nil { + log.Warn(err) + return + } + + if zoneExists(config, zoneName) { + log.Debugf("reverse DNS zone %s already exists", zoneName) + return + } + + records := collectPTRRecords(config, ipNet) + + reverseZone := nbdns.CustomZone{ + Domain: zoneName, + Records: records, + } + + config.CustomZones = append(config.CustomZones, reverseZone) + log.Debugf("added reverse DNS zone: %s with %d records", zoneName, len(records)) +} diff --git a/client/internal/dns/host.go b/client/internal/dns/host.go index fbe8c4dbb..cfc0cc3c3 100644 --- a/client/internal/dns/host.go +++ b/client/internal/dns/host.go @@ -9,6 +9,11 @@ import ( nbdns "github.com/netbirdio/netbird/dns" ) +const ( + ipv4ReverseZone = ".in-addr.arpa" + ipv6ReverseZone = ".ip6.arpa" +) + type hostManager interface { applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error restoreHostDNS() error @@ -94,9 +99,10 @@ func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) HostD } for _, customZone := range dnsConfig.CustomZones { + matchOnly := strings.HasSuffix(customZone.Domain, ipv4ReverseZone) || strings.HasSuffix(customZone.Domain, ipv6ReverseZone) config.Domains = append(config.Domains, DomainConfig{ Domain: strings.TrimSuffix(customZone.Domain, "."), - MatchOnly: false, + MatchOnly: matchOnly, }) } diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index d4d68370d..f536a1434 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -395,12 +395,12 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { localMuxUpdates, localRecordsByDomain, err := s.buildLocalHandlerUpdate(update.CustomZones) if err != nil { - return fmt.Errorf("not applying dns update, error: %v", err) + return fmt.Errorf("local handler updater: %w", err) } upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups) if err != nil { - return fmt.Errorf("not applying dns update, error: %v", err) + return fmt.Errorf("upstream handler updater: %w", err) } muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...) //nolint:gocritic @@ -447,7 +447,8 @@ func (s *DefaultServer) buildLocalHandlerUpdate( for _, customZone := range customZones { if len(customZone.Records) == 0 { - return nil, nil, fmt.Errorf("received an empty list of records") + log.Warnf("received a custom zone with empty records, skipping domain: %s", customZone.Domain) + continue } muxUpdates = append(muxUpdates, handlerWrapper{ @@ -460,7 +461,8 @@ func (s *DefaultServer) buildLocalHandlerUpdate( for _, record := range customZone.Records { var class uint16 = dns.ClassINET if record.Class != nbdns.DefaultClass { - return nil, nil, fmt.Errorf("received an invalid class type: %s", record.Class) + log.Warnf("received an invalid class type: %s", record.Class) + continue } key := buildRecordKey(record.Name, class, uint16(record.Type)) diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index e9ddd5f59..1354462d9 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -266,7 +266,7 @@ func TestUpdateDNSServer(t *testing.T) { shouldFail: true, }, { - name: "Invalid Custom Zone Records list Should Fail", + name: "Invalid Custom Zone Records list Should Skip", initLocalMap: make(registrationMap), initUpstreamMap: make(registeredHandlerMap), initSerial: 0, @@ -285,7 +285,11 @@ func TestUpdateDNSServer(t *testing.T) { }, }, }, - shouldFail: true, + expectedUpstreamMap: registeredHandlerMap{generateDummyHandler(".", nameServers).id(): handlerWrapper{ + domain: ".", + handler: dummyHandler, + priority: PriorityDefault, + }}, }, { name: "Empty Config Should Succeed and Clean Maps", diff --git a/client/internal/engine.go b/client/internal/engine.go index 90f70a827..ebb68b98b 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -953,7 +953,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { protoDNSConfig = &mgmProto.DNSConfig{} } - if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig)); err != nil { + if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig, e.wgInterface.Address().Network)); err != nil { log.Errorf("failed to update dns server, err: %v", err) } @@ -1022,7 +1022,7 @@ func toRouteDomains(myPubKey string, protoRoutes []*mgmProto.Route) []string { return dnsRoutes } -func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config { +func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig, network *net.IPNet) nbdns.Config { dnsUpdate := nbdns.Config{ ServiceEnable: protoDNSConfig.GetServiceEnable(), CustomZones: make([]nbdns.CustomZone, 0), @@ -1062,6 +1062,11 @@ func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config { } dnsUpdate.NameServerGroups = append(dnsUpdate.NameServerGroups, dnsNSGroup) } + + if len(dnsUpdate.CustomZones) > 0 { + addReverseZone(&dnsUpdate, network) + } + return dnsUpdate } @@ -1368,7 +1373,7 @@ func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { return nil, nil, err } routes := toRoutes(netMap.GetRoutes()) - dnsCfg := toDNSConfig(netMap.GetDNSConfig()) + dnsCfg := toDNSConfig(netMap.GetDNSConfig(), e.wgInterface.Address().Network) return routes, &dnsCfg, nil } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 2b1d3b098..599d36eab 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -361,6 +361,15 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { RemovePeerFunc: func(peerKey string) error { return nil }, + AddressFunc: func() iface.WGAddress { + return iface.WGAddress{ + IP: net.ParseIP("10.20.0.1"), + Network: &net.IPNet{ + IP: net.ParseIP("10.20.0.0"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + } + }, } engine.wgInterface = wgIface engine.routeManager = routemanager.NewManager(routemanager.ManagerConfig{ @@ -803,6 +812,9 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { }, }, }, + { + Domain: "0.66.100.in-addr.arpa.", + }, }, NameServerGroups: []*mgmtProto.NameServerGroup{ { @@ -832,6 +844,9 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { }, }, }, + { + Domain: "0.66.100.in-addr.arpa.", + }, }, expectedNSGroupsLen: 1, expectedNSGroups: []*nbdns.NameServerGroup{ From f00a997167fd4755673ec3559cf059b19d1b3930 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:17:42 +0000 Subject: [PATCH 23/23] [management] fix grpc new account (#3361) --- management/server/grpcserver.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 9d1bc1deb..3d170afa4 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -276,11 +276,16 @@ func (s *GRPCServer) validateToken(ctx context.Context, jwtToken string) (string } // we need to call this method because if user is new, we will automatically add it to existing or create a new account - _, _, err = s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth) + accountId, _, err := s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth) if err != nil { return "", status.Errorf(codes.Internal, "unable to fetch account with claims, err: %v", err) } + if userAuth.AccountId != accountId { + log.WithContext(ctx).Debugf("gRPC server sets accountId from ensure, before %s, now %s", userAuth.AccountId, accountId) + userAuth.AccountId = accountId + } + userAuth, err = s.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, token) if err != nil { return "", status.Error(codes.PermissionDenied, err.Error())