Skip to content

Conversation

@Germ-99
Copy link
Member

@Germ-99 Germ-99 commented Nov 26, 2025

Fixes: #168

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements restrictions on multiple device connections per Nakama account to prevent users from simultaneously joining matches as players from different devices while allowing spectator access.

Key Changes:

  • Added CountSessionsForUser() method to track active sessions per user
  • Implemented restrictions for secondary connections: spectator-only access for matches, no session creation, and no social lobby access
  • Added new error code SecondaryConnectionRestricted for handling violations

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
server/session_registry.go Added CountSessionsForUser() interface method and implementation to count active sessions per user
server/match_common_test.go Added mock implementation of CountSessionsForUser() for test registry
server/evr_lobby_session.go Added multi-session checks to restrict secondary connections to spectator role and prevent session creation
server/evr_lobby_join.go Added check to prevent secondary connections from accessing social lobbies
server/evr_lobby_find.go Added check to prevent secondary connections from finding social lobbies
server/evr_lobby_errors.go Added new error code SecondaryConnectionRestricted for multi-session violations

Comment on lines +19 to +51
// Check if this is a secondary connection and restrict it
sessionCount := p.sessionRegistry.CountSessionsForUser(session.UserID())
if sessionCount > 1 {
// This user has multiple sessions open
switch in.(type) {
case *evr.LobbyFindSessionRequest:
// For find requests, only allow spectator role
if lobbyParams.Role != evr.TeamSpectator {
logger.Warn("Secondary connection attempted to join as player",
zap.String("user_id", session.UserID().String()),
zap.String("session_id", session.ID().String()),
zap.String("role", TeamIndex(lobbyParams.Role).String()))
return NewLobbyError(SecondaryConnectionRestricted, "secondary connections can only spectate")
}

case *evr.LobbyJoinSessionRequest:
// Join requests also need spectator check if non-spectator role requested
if lobbyParams.Role != evr.TeamSpectator {
logger.Warn("Secondary connection attempted to join as player",
zap.String("user_id", session.UserID().String()),
zap.String("session_id", session.ID().String()),
zap.String("role", TeamIndex(lobbyParams.Role).String()))
return NewLobbyError(SecondaryConnectionRestricted, "secondary connections can only spectate")
}

case *evr.LobbyCreateSessionRequest:
// Secondary connections cannot create sessions
logger.Warn("Secondary connection attempted to create session",
zap.String("user_id", session.UserID().String()),
zap.String("session_id", session.ID().String()))
return NewLobbyError(SecondaryConnectionRestricted, "secondary connections cannot create sessions")
}
}
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new multi-session restriction logic introduced in this function lacks test coverage. Consider adding unit tests to verify:

  • Secondary connections (sessionCount > 1) are restricted to spectator role for LobbyFindSessionRequest
  • Secondary connections are restricted to spectator role for LobbyJoinSessionRequest
  • Secondary connections cannot create sessions (LobbyCreateSessionRequest)
  • Primary connections (sessionCount == 1) are not affected by these restrictions

Tests should be added to an appropriate EVR test file (e.g., evr_lobby_session_test.go).

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +44
// Check if this is a secondary connection and prevent social lobby access
sessionCount := p.sessionRegistry.CountSessionsForUser(session.UserID())
if sessionCount > 1 && (label.Mode == evr.ModeSocialPublic || label.Mode == evr.ModeSocialPrivate) {
logger.Warn("Secondary connection attempted to join social lobby",
zap.String("user_id", session.UserID().String()),
zap.String("session_id", session.ID().String()),
zap.String("mode", label.Mode.String()))
return NewLobbyError(SecondaryConnectionRestricted, "secondary connections cannot access social lobbies")
}
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The social lobby restriction for secondary connections lacks test coverage. Consider adding unit tests to verify:

  • Secondary connections are blocked from joining social lobbies (both ModeSocialPublic and ModeSocialPrivate)
  • Primary connections can join social lobbies normally
  • The correct error is returned when restrictions apply

Tests should be added to an appropriate EVR test file (e.g., evr_lobby_join_test.go).

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +41
// Check if this is a secondary connection and prevent social lobby access
sessionCount := p.sessionRegistry.CountSessionsForUser(session.UserID())
if sessionCount > 1 && lobbyParams.Mode == evr.ModeSocialPublic {
logger.Warn("Secondary connection attempted to access social lobby",
zap.String("user_id", session.UserID().String()),
zap.String("session_id", session.ID().String()))
return NewLobbyError(SecondaryConnectionRestricted, "secondary connections cannot access social lobbies")
}
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The social lobby restriction for secondary connections lacks test coverage. Consider adding unit tests to verify:

  • Secondary connections are blocked from finding social lobbies
  • Primary connections can find social lobbies normally
  • The correct error is returned when restrictions apply

Tests should be added to an appropriate EVR test file (e.g., evr_lobby_find_test.go).

Copilot uses AI. Check for mistakes.

// Check if this is a secondary connection and prevent social lobby access
sessionCount := p.sessionRegistry.CountSessionsForUser(session.UserID())
if sessionCount > 1 && lobbyParams.Mode == evr.ModeSocialPublic {
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent restriction logic: In evr_lobby_join.go (line 38), the check includes both ModeSocialPublic and ModeSocialPrivate, but here only ModeSocialPublic is checked. Secondary connections should be restricted from ModeSocialPrivate lobbies as well for consistency.

Consider changing this to:

if sessionCount > 1 && (lobbyParams.Mode == evr.ModeSocialPublic || lobbyParams.Mode == evr.ModeSocialPrivate) {
Suggested change
if sessionCount > 1 && lobbyParams.Mode == evr.ModeSocialPublic {
if sessionCount > 1 && (lobbyParams.Mode == evr.ModeSocialPublic || lobbyParams.Mode == evr.ModeSocialPrivate) {

Copilot uses AI. Check for mistakes.
var err error
var matchID MatchID

// Check if this is a secondary connection and restrict it
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace at end of line

Suggested change
// Check if this is a secondary connection and restrict it
// Check if this is a secondary connection and restrict it

Copilot uses AI. Check for mistakes.
Comment on lines +187 to +196
func (r *LocalSessionRegistry) CountSessionsForUser(userID uuid.UUID) int {
count := 0
r.sessions.Range(func(id uuid.UUID, session Session) bool {
if session.UserID() == userID {
count++
}
return true
})
return count
}
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CountSessionsForUser function iterates through all active sessions on every call. This could become a performance bottleneck with many concurrent sessions, especially since this method is called for every lobby request.

Consider:

  1. Maintaining a separate index map from userID to session count that's updated in Add() and Remove() methods for O(1) lookups
  2. Or caching the result with appropriate invalidation when sessions are added/removed

Copilot uses AI. Check for mistakes.
Copy link
Member

@thesprockee thesprockee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot implement changes based on the review. use the stream count for the user.

use something akin to nk.StreamCount(StreamModeService, <user_id>, "", StreamLabelMatchService) to see if the user has any other match connections active.

SuspendedFromLobbyGroup
KickedFromLobbyGroup
NotALobbyGroupMod
SecondaryConnectionRestricted
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not add to the LobbyErrorCodeValue's, as they are hard coded in the EVR client.

SuspendedFromLobbyGroup: "suspended_from_lobby_group",
KickedFromLobbyGroup: "kicked_from_lobby_group",
NotALobbyGroupMod: "not_a_lobby_group_mod",
SecondaryConnectionRestricted: "secondary_connection_restricted",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not add to the LobbyErrorCodeValue's, as they are hard coded in the EVR client.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an "upstream" file, do not modify it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an "upstream" file, do not modify it.

return NewLobbyError(SecondaryConnectionRestricted, "secondary connections can only spectate")
}

case *evr.LobbyCreateSessionRequest:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allow secondary connections to create sessions, as long as they are spectators.


startTime := time.Now()

// Check if this is a secondary connection and prevent social lobby access
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is duplicated in server/evr_lobby_session.go

lobbyParams.GroupID = label.GetGroupID()
lobbyParams.Mode = label.Mode

// Check if this is a secondary connection and prevent social lobby access
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is duplicated from server/evr_lobby_session.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restrict multiple devices per Nakama account: enforce spectator-only for additional connections

2 participants