mirror of https://github.com/jetkvm/kvm.git
feat: add strict observer-to-primary promotion controls and immediate logout promotion
Observer-to-primary promotion protections: - Block auto-promotion during active primary grace periods - Prevent creating multiple primary sessions simultaneously - Validate transfer source is actual current primary - Check for duplicate primaries before promotion Immediate promotion on logout: - Trigger validateSinglePrimary() immediately when primary disconnects - Smart grace period bypass: allow promotion within 2 seconds of disconnect - Provides instant promotion on logout while protecting against network blips Enhanced validation and logging: - Log session additions/removals with counts - Display session IDs in validation logs for debugging - Track grace period timing for smart bypass decisions
This commit is contained in:
parent
ffc4a2af21
commit
f9ebd6ac2f
|
|
@ -255,6 +255,20 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
// and assign primary status atomically to prevent race conditions
|
||||
primaryExists := sm.primarySessionID != "" && sm.sessions[sm.primarySessionID] != nil
|
||||
|
||||
// Check if there's an active grace period for a primary session (different from this session)
|
||||
hasActivePrimaryGracePeriod := false
|
||||
if sm.lastPrimaryID != "" && sm.lastPrimaryID != session.ID {
|
||||
if graceTime, exists := sm.reconnectGrace[sm.lastPrimaryID]; exists {
|
||||
if time.Now().Before(graceTime) {
|
||||
if reconnectInfo, hasInfo := sm.reconnectInfo[sm.lastPrimaryID]; hasInfo {
|
||||
if reconnectInfo.Mode == SessionModePrimary {
|
||||
hasActivePrimaryGracePeriod = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this session was recently demoted via transfer
|
||||
isBlacklisted := sm.isSessionBlacklisted(session.ID)
|
||||
|
||||
|
|
@ -263,6 +277,7 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
Str("nickname", session.Nickname).
|
||||
Str("currentPrimarySessionID", sm.primarySessionID).
|
||||
Bool("primaryExists", primaryExists).
|
||||
Bool("hasActivePrimaryGracePeriod", hasActivePrimaryGracePeriod).
|
||||
Int("totalSessions", len(sm.sessions)).
|
||||
Bool("wasWithinGracePeriod", wasWithinGracePeriod).
|
||||
Bool("wasPreviouslyPrimary", wasPreviouslyPrimary).
|
||||
|
|
@ -271,10 +286,10 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
Msg("AddSession state analysis")
|
||||
|
||||
// Become primary only if:
|
||||
// 1. Was previously primary (within grace) AND no current primary, OR
|
||||
// 2. There's no primary at all AND not recently transferred away
|
||||
// Never allow primary promotion if already restored within grace period
|
||||
shouldBecomePrimary := !wasWithinGracePeriod && ((wasPreviouslyPrimary && !primaryExists) || (!primaryExists && !isBlacklisted))
|
||||
// 1. Was previously primary (within grace) AND no current primary AND no other session has grace period, OR
|
||||
// 2. There's no primary at all AND not recently transferred away AND no active grace period
|
||||
// Never allow primary promotion if already restored within grace period or another session has grace period
|
||||
shouldBecomePrimary := !wasWithinGracePeriod && !hasActivePrimaryGracePeriod && ((wasPreviouslyPrimary && !primaryExists) || (!primaryExists && !isBlacklisted))
|
||||
|
||||
if wasWithinGracePeriod {
|
||||
sm.logger.Debug().
|
||||
|
|
@ -354,6 +369,12 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
// This ensures that primary existence checks work correctly during restoration
|
||||
sm.sessions[session.ID] = session
|
||||
|
||||
sm.logger.Info().
|
||||
Str("sessionID", session.ID).
|
||||
Str("mode", string(session.Mode)).
|
||||
Int("totalSessions", len(sm.sessions)).
|
||||
Msg("Session added to manager")
|
||||
|
||||
// Ensure session has auto-generated nickname if needed
|
||||
sm.ensureNickname(session)
|
||||
|
||||
|
|
@ -373,12 +394,21 @@ func (sm *SessionManager) RemoveSession(sessionID string) {
|
|||
|
||||
session, exists := sm.sessions[sessionID]
|
||||
if !exists {
|
||||
sm.logger.Debug().
|
||||
Str("sessionID", sessionID).
|
||||
Msg("RemoveSession called but session not found in map")
|
||||
return
|
||||
}
|
||||
|
||||
wasPrimary := session.Mode == SessionModePrimary
|
||||
delete(sm.sessions, sessionID)
|
||||
|
||||
sm.logger.Info().
|
||||
Str("sessionID", sessionID).
|
||||
Bool("wasPrimary", wasPrimary).
|
||||
Int("remainingSessions", len(sm.sessions)).
|
||||
Msg("Session removed from manager")
|
||||
|
||||
// Remove from queue if present
|
||||
sm.removeFromQueue(sessionID)
|
||||
|
||||
|
|
@ -428,10 +458,18 @@ func (sm *SessionManager) RemoveSession(sessionID string) {
|
|||
sm.logger.Info().
|
||||
Str("sessionID", sessionID).
|
||||
Dur("gracePeriod", time.Duration(gracePeriod)*time.Second).
|
||||
Msg("Primary session removed, grace period active - auto-promotion will occur after grace expires")
|
||||
Int("remainingSessions", len(sm.sessions)).
|
||||
Msg("Primary session removed, grace period active")
|
||||
|
||||
// NOTE: Do NOT call validateSinglePrimary() here - let grace period expire naturally
|
||||
// The cleanupInactiveSessions() function will handle promotion after grace period expires
|
||||
// Immediate promotion check: if there are observers waiting, trigger validation
|
||||
// This allows immediate promotion while still respecting grace period protection
|
||||
if len(sm.sessions) > 0 {
|
||||
sm.logger.Debug().
|
||||
Str("removedPrimaryID", sessionID).
|
||||
Int("remainingSessions", len(sm.sessions)).
|
||||
Msg("Triggering immediate validation for potential promotion")
|
||||
sm.validateSinglePrimary()
|
||||
}
|
||||
}
|
||||
|
||||
// Notify remaining sessions
|
||||
|
|
@ -704,6 +742,20 @@ func (sm *SessionManager) TransferPrimary(fromID, toID string) error {
|
|||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
// SECURITY: Verify fromID is the actual current primary
|
||||
if sm.primarySessionID != fromID {
|
||||
return fmt.Errorf("transfer denied: %s is not the current primary (current primary: %s)", fromID, sm.primarySessionID)
|
||||
}
|
||||
|
||||
fromSession, exists := sm.sessions[fromID]
|
||||
if !exists {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
|
||||
if fromSession.Mode != SessionModePrimary {
|
||||
return errors.New("transfer denied: from session is not in primary mode")
|
||||
}
|
||||
|
||||
// Use centralized transfer method
|
||||
err := sm.transferPrimaryRole(fromID, toID, "direct_transfer", "manual transfer request")
|
||||
if err != nil {
|
||||
|
|
@ -899,20 +951,64 @@ func (sm *SessionManager) validateSinglePrimary() {
|
|||
sm.primarySessionID = ""
|
||||
}
|
||||
|
||||
// Check if there's an active grace period for any primary session
|
||||
// BUT: if grace period just started (within 2 seconds), allow immediate promotion
|
||||
hasActivePrimaryGracePeriod := false
|
||||
for sessionID, graceTime := range sm.reconnectGrace {
|
||||
if time.Now().Before(graceTime) {
|
||||
if reconnectInfo, hasInfo := sm.reconnectInfo[sessionID]; hasInfo {
|
||||
if reconnectInfo.Mode == SessionModePrimary {
|
||||
// Calculate how long ago the grace period started
|
||||
gracePeriod := 10
|
||||
if currentSessionSettings != nil && currentSessionSettings.ReconnectGrace > 0 {
|
||||
gracePeriod = currentSessionSettings.ReconnectGrace
|
||||
}
|
||||
graceStartTime := graceTime.Add(-time.Duration(gracePeriod) * time.Second)
|
||||
timeSinceGraceStart := time.Since(graceStartTime)
|
||||
|
||||
// If grace period just started (within 2 seconds), allow immediate promotion
|
||||
// This enables instant promotion on logout while still protecting against network blips
|
||||
if timeSinceGraceStart > 2*time.Second {
|
||||
hasActivePrimaryGracePeriod = true
|
||||
sm.logger.Debug().
|
||||
Str("gracePrimaryID", sessionID).
|
||||
Dur("remainingGrace", time.Until(graceTime)).
|
||||
Dur("timeSinceGraceStart", timeSinceGraceStart).
|
||||
Msg("Active grace period detected for primary session - blocking auto-promotion")
|
||||
} else {
|
||||
sm.logger.Debug().
|
||||
Str("gracePrimaryID", sessionID).
|
||||
Dur("timeSinceGraceStart", timeSinceGraceStart).
|
||||
Msg("Grace period just started - allowing immediate promotion")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build session IDs list for debugging
|
||||
sessionIDs := make([]string, 0, len(sm.sessions))
|
||||
for id := range sm.sessions {
|
||||
sessionIDs = append(sessionIDs, id)
|
||||
}
|
||||
|
||||
sm.logger.Debug().
|
||||
Int("primarySessionCount", len(primarySessions)).
|
||||
Str("primarySessionID", sm.primarySessionID).
|
||||
Int("totalSessions", len(sm.sessions)).
|
||||
Strs("sessionIDs", sessionIDs).
|
||||
Bool("hasActivePrimaryGracePeriod", hasActivePrimaryGracePeriod).
|
||||
Msg("validateSinglePrimary state check")
|
||||
|
||||
// Auto-promote if there are NO primary sessions at all
|
||||
if len(primarySessions) == 0 && sm.primarySessionID == "" && len(sm.sessions) > 0 {
|
||||
// Auto-promote if there are NO primary sessions at all AND no active grace period
|
||||
if len(primarySessions) == 0 && sm.primarySessionID == "" && len(sm.sessions) > 0 && !hasActivePrimaryGracePeriod {
|
||||
// Find a session to promote to primary
|
||||
nextSessionID := sm.findNextSessionToPromote()
|
||||
if nextSessionID != "" {
|
||||
sm.logger.Info().
|
||||
Str("promotedSessionID", nextSessionID).
|
||||
Msg("Auto-promoting observer to primary - no primary sessions exist")
|
||||
Msg("Auto-promoting observer to primary - no primary sessions exist and no grace period active")
|
||||
|
||||
// Use the centralized promotion logic
|
||||
err := sm.transferPrimaryRole("", nextSessionID, "emergency_auto_promotion", "no primary sessions detected")
|
||||
|
|
@ -931,6 +1027,7 @@ func (sm *SessionManager) validateSinglePrimary() {
|
|||
Int("primarySessions", len(primarySessions)).
|
||||
Str("primarySessionID", sm.primarySessionID).
|
||||
Bool("hasSessions", len(sm.sessions) > 0).
|
||||
Bool("hasActivePrimaryGracePeriod", hasActivePrimaryGracePeriod).
|
||||
Msg("Emergency auto-promotion conditions not met")
|
||||
}
|
||||
}
|
||||
|
|
@ -944,6 +1041,15 @@ func (sm *SessionManager) transferPrimaryRole(fromSessionID, toSessionID, transf
|
|||
return ErrSessionNotFound
|
||||
}
|
||||
|
||||
// SECURITY: Prevent promoting a session that's already primary
|
||||
if toSession.Mode == SessionModePrimary {
|
||||
sm.logger.Warn().
|
||||
Str("sessionID", toSessionID).
|
||||
Str("transferType", transferType).
|
||||
Msg("Attempted to promote session that is already primary")
|
||||
return errors.New("target session is already primary")
|
||||
}
|
||||
|
||||
var fromSession *Session
|
||||
var fromExists bool
|
||||
if fromSessionID != "" {
|
||||
|
|
@ -967,6 +1073,18 @@ func (sm *SessionManager) transferPrimaryRole(fromSessionID, toSessionID, transf
|
|||
Msg("Demoted existing primary session")
|
||||
}
|
||||
|
||||
// SECURITY: Before promoting, verify there are no other primary sessions
|
||||
for id, sess := range sm.sessions {
|
||||
if id != toSessionID && sess.Mode == SessionModePrimary {
|
||||
sm.logger.Error().
|
||||
Str("existingPrimaryID", id).
|
||||
Str("targetPromotionID", toSessionID).
|
||||
Str("transferType", transferType).
|
||||
Msg("CRITICAL: Attempted to create second primary - blocking promotion")
|
||||
return fmt.Errorf("cannot promote: another primary session exists (%s)", id)
|
||||
}
|
||||
}
|
||||
|
||||
// Promote target session
|
||||
toSession.Mode = SessionModePrimary
|
||||
toSession.hidRPCAvailable = false // Force re-handshake
|
||||
|
|
|
|||
Loading…
Reference in New Issue