From cf4b9f8333c7b0c44e3f3cf391f0fde6cc86d135 Mon Sep 17 00:00:00 2001 From: Dmitri Dolguikh Date: Thu, 20 Aug 2026 16:06:50 +0200 Subject: [PATCH] use peer.Status.LastSeen field to detect stale peer.Status.Connected values Signed-off-by: Dmitri Dolguikh --- management/internals/shared/grpc/server.go | 8 ++++++++ management/server/account/manager.go | 1 + management/server/account/manager_mock.go | 14 ++++++++++++++ management/server/mock_server/account_mock.go | 8 ++++++++ management/server/peer.go | 5 +++++ management/server/store/sql_store.go | 3 +++ 6 files changed, 39 insertions(+) diff --git a/management/internals/shared/grpc/server.go b/management/internals/shared/grpc/server.go index 485f05a92..4452ccfb0 100644 --- a/management/internals/shared/grpc/server.go +++ b/management/internals/shared/grpc/server.go @@ -423,6 +423,7 @@ func (s *Server) handleUpdates(ctx context.Context, accountID string, peerKey wg // Create a debouncer for this peer connection debouncer := NewUpdateDebouncer(1000 * time.Millisecond) defer debouncer.Stop() + ticker := time.NewTicker(1 * time.Second) for { select { @@ -448,6 +449,8 @@ func (s *Server) handleUpdates(ctx context.Context, accountID string, peerKey wg } } + s.accountManager.RefreshPeerLastSeen(ctx, accountID, peer.ID) + // Timer expired - quiet period reached, send pending updates if any case <-debouncer.TimerChannel(): pendingUpdates := debouncer.GetPendingUpdates() @@ -462,6 +465,11 @@ func (s *Server) handleUpdates(ctx context.Context, accountID string, peerKey wg } } + s.accountManager.RefreshPeerLastSeen(ctx, accountID, peer.ID) + + case <-ticker.C: + s.accountManager.RefreshPeerLastSeen(ctx, accountID, peer.ID) + // condition when client <-> server connection has been terminated case <-srv.Context().Done(): // happens when connection drops, e.g. client disconnects diff --git a/management/server/account/manager.go b/management/server/account/manager.go index f4b0408cf..2b59153e1 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -64,6 +64,7 @@ type Manager interface { GetPeers(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) MarkPeerConnected(ctx context.Context, peerKey string, accountID string, sessionStartedAt int64, nmap *types.NetworkMap) error MarkPeerDisconnected(ctx context.Context, peerKey string, accountID string, sessionStartedAt int64) error + RefreshPeerLastSeen(ctx context.Context, accountId, peerId string) error DeletePeer(ctx context.Context, accountID, peerID, userID string) error UpdatePeer(ctx context.Context, accountID, userID string, p *nbpeer.Peer) (*nbpeer.Peer, error) UpdatePeerIP(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error diff --git a/management/server/account/manager_mock.go b/management/server/account/manager_mock.go index 9ac10cba0..9b2b5d60e 100644 --- a/management/server/account/manager_mock.go +++ b/management/server/account/manager_mock.go @@ -1382,6 +1382,20 @@ func (mr *MockManagerMockRecorder) OnPeerDisconnected(ctx, accountID, peerPubKey return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeerDisconnected", reflect.TypeOf((*MockManager)(nil).OnPeerDisconnected), ctx, accountID, peerPubKey, streamStartTime) } +// RefreshPeerLastSeen mocks base method. +func (m *MockManager) RefreshPeerLastSeen(ctx context.Context, accountId, peerId string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RefreshPeerLastSeen", ctx, accountId, peerId) + ret0, _ := ret[0].(error) + return ret0 +} + +// RefreshPeerLastSeen indicates an expected call of RefreshPeerLastSeen. +func (mr *MockManagerMockRecorder) RefreshPeerLastSeen(ctx, accountId, peerId any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshPeerLastSeen", reflect.TypeOf((*MockManager)(nil).RefreshPeerLastSeen), ctx, accountId, peerId) +} + // RegenerateUserInvite mocks base method. func (m *MockManager) RegenerateUserInvite(ctx context.Context, accountID, initiatorUserID, inviteID string, expiresIn int) (*types.UserInvite, error) { m.ctrl.T.Helper() diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 071e3771b..ed36a04cc 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -41,6 +41,7 @@ type MockAccountManager struct { GetPeersFunc func(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) MarkPeerConnectedFunc func(ctx context.Context, peerKey string, accountID string, sessionStartedAt int64, nmap *types.NetworkMap) error MarkPeerDisconnectedFunc func(ctx context.Context, peerKey string, accountID string, sessionStartedAt int64) error + RefreshPeerLastSeenFunc func(ctx context.Context, accountId, peerId string) error SyncAndMarkPeerFunc func(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP, syncTime time.Time) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) DeletePeerFunc func(ctx context.Context, accountID, peerKey, userID string) error GetNetworkMapFunc func(ctx context.Context, peerKey string) (*types.NetworkMap, error) @@ -360,6 +361,13 @@ func (am *MockAccountManager) MarkPeerDisconnected(ctx context.Context, peerKey return status.Errorf(codes.Unimplemented, "method MarkPeerDisconnected is not implemented") } +func (am *MockAccountManager) RefreshPeerLastSeen(ctx context.Context, accountId, peerId string) error { + if am.RefreshPeerLastSeenFunc != nil { + return am.RefreshPeerLastSeenFunc(ctx, accountId, peerId) + } + return status.Errorf(codes.Unimplemented, "method RefreshPeerLastSeen 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 { diff --git a/management/server/peer.go b/management/server/peer.go index 589cf9abf..145ad9a4e 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -136,6 +136,11 @@ func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubK return nil } +func (am *DefaultAccountManager) RefreshPeerLastSeen(ctx context.Context, accountId, peerId string) error { + _, err := am.Store.RefreshPeerLastSeen(ctx, accountId, peerId, time.Now()) + return err +} + // schedulePeerExpirations reschedules the account's login/inactivity expiration // timers for an SSO peer that just connected. func (am *DefaultAccountManager) schedulePeerExpirations(ctx context.Context, accountID string, peer *nbpeer.Peer) error { diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 99bb2c2c1..44b76d0b9 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -1962,6 +1962,9 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee } if peerStatusConnected.Valid { p.Status.Connected = peerStatusConnected.Bool + if peerStatusLastSeen.Valid { + p.Status.Connected = p.Status.Connected && peerStatusLastSeen.Time.Before(time.Now().Add(-5*time.Minute)) + } } if peerStatusLoginExpired.Valid { p.Status.LoginExpired = peerStatusLoginExpired.Bool