From 809a258b7a0dcf868afb52cbad79baf398f356d3 Mon Sep 17 00:00:00 2001 From: mkwcat Date: Wed, 10 Jan 2024 21:24:26 -0500 Subject: [PATCH] QR2/GPCM: Add protection for restricted players joining public rooms --- gpcm/friend.go | 20 +++++++++++++++ gpcm/kick.go | 12 ++++++--- gpcm/login.go | 2 +- qr2/group.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ qr2/heartbeat.go | 12 ++++++--- qr2/logins.go | 4 ++- qr2/message.go | 25 ++++++++++++++++++ 7 files changed, 132 insertions(+), 10 deletions(-) diff --git a/gpcm/friend.go b/gpcm/friend.go index 554c428..a401eba 100644 --- a/gpcm/friend.go +++ b/gpcm/friend.go @@ -478,6 +478,26 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) { return } + if g.User.Restricted || toSession.User.Restricted { + // Check with QR2 if the room is public or private + resvError := qr2.CheckGPReservationAllowed(g.QR2IP, g.User.ProfileId, uint32(toProfileId), msgMatchData.Reservation.MatchType) + if resvError != "ok" { + if resvError == "restricted" || resvError == "restricted_join" { + logging.Error(g.ModuleName, "RESERVATION: Restricted user tried to connect to public room") + + // Kick the player(s) + if g.User.Restricted { + kickPlayer(toSession.User.ProfileId, resvError) + } + if toSession.User.Restricted { + kickPlayer(g.User.ProfileId, resvError) + } + } + // Otherwise generic error? + return + } + } + if !sameAddress { searchId := qr2.GetSearchID(g.QR2IP) msgMatchData.Reservation.PublicIP = uint32(searchId & 0xffffffff) diff --git a/gpcm/kick.go b/gpcm/kick.go index 4aefd0a..8b17e5d 100644 --- a/gpcm/kick.go +++ b/gpcm/kick.go @@ -1,9 +1,6 @@ package gpcm -func KickPlayer(profileID uint32, reason string) { - mutex.Lock() - defer mutex.Unlock() - +func kickPlayer(profileID uint32, reason string) { if session, exists := sessions[profileID]; exists { errorMessage := WWFCMsgKickedGeneric @@ -33,3 +30,10 @@ func KickPlayer(profileID uint32, reason string) { session.Conn.Close() } } + +func KickPlayer(profileID uint32, reason string) { + mutex.Lock() + defer mutex.Unlock() + + kickPlayer(profileID, reason) +} diff --git a/gpcm/login.go b/gpcm/login.go index 1279e82..b103e5f 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -248,7 +248,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { g.ModuleName += "/" + common.CalcFriendCodeString(g.User.ProfileId, "RMCJ") // Notify QR2 of the login - qr2.Login(g.User.ProfileId, gamecd, ingamesn, cfc, g.Conn.RemoteAddr().String(), g.NeedsExploit, g.DeviceAuthenticated, g.User.Restricted) + qr2.Login(g.User.ProfileId, gamecd, ingamesn, cfc, g.Conn.RemoteAddr().String(), g.NeedsExploit, g.DeviceAuthenticated, g.User.Restricted, KickPlayer) payload := common.CreateGameSpyMessage(common.GameSpyCommand{ Command: "lc", diff --git a/qr2/group.go b/qr2/group.go index db4c848..d75a392 100644 --- a/qr2/group.go +++ b/qr2/group.go @@ -185,6 +185,73 @@ func ProcessGPStatusUpdate(profileID uint32, senderIP uint64, status string) { } } +func checkReservationAllowed(moduleName string, sender, destination *Session, joinType byte) string { + if sender.Login == nil || destination.Login == nil { + return "" + } + + if !sender.Login.Restricted && !destination.Login.Restricted { + return "ok" + } + + if joinType != 2 && joinType != 3 { + return "restricted_join" + } + + // TODO: Once OpenHost is implemented, disallow joining public rooms + + if destination.GroupPointer == nil { + // Destination is not in a group, check their dwc_mtype instead + if destination.Data["dwc_mtype"] != "2" && destination.Data["dwc_mtype"] != "3" { + return "restricted_join" + } + + // This is fine + return "ok" + } + + if destination.GroupPointer.MatchType != "private" { + return "restricted_join" + } + + return "ok" +} + +func CheckGPReservationAllowed(senderIP uint64, senderPid uint32, destPid uint32, joinType byte) string { + senderPidStr := strconv.FormatUint(uint64(senderPid), 10) + destPidStr := strconv.FormatUint(uint64(destPid), 10) + + moduleName := "QR2:CheckReservation:" + senderPidStr + "->" + destPidStr + + mutex.Lock() + defer mutex.Unlock() + + from := sessions[senderIP] + if from == nil { + logging.Error(moduleName, "Sender IP does not exist:", aurora.Cyan(fmt.Sprintf("%012x", senderIP))) + return "" + } + + toLogin := logins[destPid] + if toLogin == nil { + logging.Error(moduleName, "Destination profile ID does not exist:", aurora.Cyan(destPid)) + return "" + } + + to := toLogin.Session + if to == nil { + logging.Error(moduleName, "Destination profile ID does not have a session") + return "" + } + + // Validate dwc_pid value + if !from.setProfileID(moduleName, senderPidStr) || from.Login == nil { + return "" + } + + return checkReservationAllowed(moduleName, from, to, joinType) +} + // findNewServer attempts to find the new server/host in the group when the current server goes down. // If no server is found, the group's server pointer is set to nil. // Expects the mutex to be locked. diff --git a/qr2/heartbeat.go b/qr2/heartbeat.go index cccbb1a..77446ad 100644 --- a/qr2/heartbeat.go +++ b/qr2/heartbeat.go @@ -12,8 +12,6 @@ import ( func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []byte) { sessionId := binary.BigEndian.Uint32(buffer[1:5]) - logging.Info(moduleName, "Received heartbeat; session ID:", aurora.BrightCyan(sessionId)) - values := strings.Split(string(buffer[5:]), "\u0000") payload := map[string]string{} @@ -31,6 +29,14 @@ func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []b payload[values[i]] = values[i+1] } + if payload["dwc_mtype"] != "" { + logging.Info(moduleName, "Match type:", aurora.Cyan(payload["dwc_mtype"])) + } + + if payload["dwc_hoststate"] != "" { + logging.Info(moduleName, "Host state:", aurora.Cyan(payload["dwc_hoststate"])) + } + realIP, realPort := common.IPFormatToString(addr.String()) if ip, ok := payload["publicip"]; !ok || ip == "0" { @@ -94,6 +100,4 @@ func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []b } } mutex.Unlock() - - logging.Info(moduleName, "Heartbeat ok") } diff --git a/qr2/logins.go b/qr2/logins.go index 90fe2a9..7f2b2e6 100644 --- a/qr2/logins.go +++ b/qr2/logins.go @@ -9,12 +9,13 @@ type LoginInfo struct { NeedsExploit bool DeviceAuthenticated bool Restricted bool + GPErrorCallback func(uint32, string) Session *Session } var logins = map[uint32]*LoginInfo{} -func Login(profileID uint32, gameCode string, inGameName string, consoleFriendCode uint64, publicIP string, needsExploit bool, deviceAuthenticated bool, restricted bool) { +func Login(profileID uint32, gameCode string, inGameName string, consoleFriendCode uint64, publicIP string, needsExploit bool, deviceAuthenticated bool, restricted bool, gpErrorCallback func(uint32, string)) { mutex.Lock() defer mutex.Unlock() @@ -27,6 +28,7 @@ func Login(profileID uint32, gameCode string, inGameName string, consoleFriendCo NeedsExploit: needsExploit, DeviceAuthenticated: deviceAuthenticated, Restricted: restricted, + GPErrorCallback: gpErrorCallback, Session: nil, } } diff --git a/qr2/message.go b/qr2/message.go index b9a913d..c374ee0 100644 --- a/qr2/message.go +++ b/qr2/message.go @@ -245,6 +245,31 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) { common.LogMatchCommand(moduleName, destPid, cmd, matchData) if cmd == common.MatchReservation { + resvError := checkReservationAllowed(moduleName, sender, receiver, matchData.Reservation.MatchType) + if resvError != "ok" { + if resvError == "restricted" || resvError == "restricted_join" { + logging.Error(moduleName, "RESERVATION: Restricted player attempted to join a public match") + + if sender.Login != nil && sender.Login.Restricted { + callback := sender.Login.GPErrorCallback + profileId := sender.Login.ProfileID + + mutex.Unlock() + callback(profileId, resvError) + mutex.Lock() + } + if receiver.Login != nil && receiver.Login.Restricted { + callback := receiver.Login.GPErrorCallback + profileId := receiver.Login.ProfileID + + mutex.Unlock() + callback(profileId, resvError) + mutex.Lock() + } + } + return + } + sender.Reservation = matchData sender.ReservationID = receiver.SearchID } else if cmd == common.MatchResvOK || cmd == common.MatchResvDeny || cmd == common.MatchResvWait {