From 85b917951d696ff107e472fce9d4321ad4021ccd Mon Sep 17 00:00:00 2001 From: mkwcat Date: Wed, 1 Nov 2023 10:12:47 -0400 Subject: [PATCH] QR2: Verify profile ID and public IP --- gpcm/login.go | 20 ++++++++ qr2/heartbeat.go | 23 +++------ qr2/main.go | 12 +++-- qr2/session.go | 111 +++++++++++++++++++++++++++++++++++----- serverbrowser/filter.go | 13 +++-- 5 files changed, 143 insertions(+), 36 deletions(-) diff --git a/gpcm/login.go b/gpcm/login.go index 7d2b27c..349c32a 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -90,3 +90,23 @@ func (g *GameSpySession) Login(pool *pgxpool.Pool, ctx context.Context, command }, }), true } + +func IsLoggedIn(profileID uint32) bool { + mutex.Lock() + defer mutex.Unlock() + + session, exists := sessions[profileID] + return exists && session.LoggedIn +} + +func GetSessionIP(profileID uint32) string { + mutex.Lock() + defer mutex.Unlock() + + session, exists := sessions[profileID] + if exists && session.LoggedIn { + return session.Conn.RemoteAddr().String() + } + + return "" +} diff --git a/qr2/heartbeat.go b/qr2/heartbeat.go index 10a6b96..5d5abc2 100644 --- a/qr2/heartbeat.go +++ b/qr2/heartbeat.go @@ -6,6 +6,7 @@ import ( "net" "strconv" "strings" + "wwfc/common" "wwfc/logging" ) @@ -42,21 +43,7 @@ func heartbeat(conn net.PacketConn, addr net.Addr, buffer []byte) { } } - addrString := strings.Split(addr.String(), ":") - - var rawIP int - for i, s := range strings.Split(addrString[0], ".") { - val, err := strconv.Atoi(s) - if err != nil { - panic(err) - } - - rawIP |= val << (24 - i*8) - } - - // TODO: Check if this handles negative numbers correctly - realIP := strconv.FormatInt(int64(int32(rawIP)), 10) - realPort := addrString[1] + realIP, realPort := common.IPFormatToString(addr.String()) publicIPKey, hasPublicIPKey := payload["publicip"] if !hasPublicIPKey || publicIPKey != realIP { @@ -65,7 +52,11 @@ func heartbeat(conn net.PacketConn, addr net.Addr, buffer []byte) { payload["publicport"] = realPort } - session := setSessionData(sessionId, payload) + session, ok := setSessionData(sessionId, payload) + if !ok { + return + } + if !session.Authenticated || !hasPublicIPKey || publicIPKey != realIP { logging.Notice(moduleName, "Sending challenge") sendChallenge(conn, addr, session) diff --git a/qr2/main.go b/qr2/main.go index df1da27..4425300 100644 --- a/qr2/main.go +++ b/qr2/main.go @@ -78,9 +78,15 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) { logging.Notice(moduleName, "Command:", aurora.Yellow("CHALLENGE")) mutex.Lock() - sessions[sessionId].Authenticated = true - mutex.Unlock() - conn.WriteTo(createResponseHeader(ClientRegisteredReply, sessionId), addr) + if sessions[sessionId].Challenge != "" { + // TODO: Verify the challenge + sessions[sessionId].Authenticated = true + mutex.Unlock() + + conn.WriteTo(createResponseHeader(ClientRegisteredReply, sessionId), addr) + } else { + mutex.Unlock() + } break case EchoRequest: diff --git a/qr2/session.go b/qr2/session.go index e010135..fec89fe 100644 --- a/qr2/session.go +++ b/qr2/session.go @@ -7,6 +7,8 @@ import ( "strconv" "strings" "time" + "wwfc/common" + "wwfc/gpcm" "wwfc/logging" ) @@ -32,13 +34,86 @@ func removeSession(sessionId uint32) { } // Update session data, creating the session if it doesn't exist. Returns a copy of the session data. -func setSessionData(sessionId uint32, payload map[string]string) Session { +func setSessionData(sessionId uint32, payload map[string]string) (Session, bool) { + moduleName := "QR2:" + strconv.FormatInt(int64(sessionId), 10) + + // Perform sanity checks on the session data. This is a mess but + + // This is an internal error that should not happen + publicIP := "" + var ok bool + if publicIP, ok = payload["publicip"]; !ok || publicIP == "0" { + logging.Error(moduleName, "Missing publicip in session data") + return Session{}, false + } + + newPID, newPIDValid := payload["dwc_pid"] + + // Moving into performing operations on the session data, so lock the mutex mutex.Lock() defer mutex.Unlock() + session, sessionExists := sessions[sessionId] - session, exists := sessions[sessionId] - if !exists { - logging.Notice("QR2", "Creating session", aurora.Cyan(sessionId).String()) + if newPIDValid { + var oldPID string + oldPIDValid := false + + if sessionExists { + if oldPID, oldPIDValid = session.Data["dwc_pid"]; oldPIDValid && newPID != oldPID { + logging.Error(moduleName, "New dwc_pid mismatch: new:", aurora.Cyan(newPID), "old:", aurora.Cyan(oldPID)) + return Session{}, false + } + } + + if !oldPIDValid { + // Setting a new PID so validate it + profileID, err := strconv.ParseUint(newPID, 10, 32) + if err != nil { + logging.Error(moduleName, "Invalid dwc_pid value:", aurora.Cyan(newPID)) + return Session{}, false + } + + // Reformat dwc_pid string + newPID = strconv.FormatUint(uint64(profileID), 10) + payload["dwc_pid"] = newPID + + // Lookup the profile ID in GPCM and verify it's logged in. + // Maybe we don't need this? It relies on GPCM being hosted in the same application, and + // makes GPCM a dependency of QR2. Perhaps we could use the database. + gpcmIP := gpcm.GetSessionIP(uint32(profileID)) + if gpcmIP == "" { + logging.Error(moduleName, "Provided dwc_pid is not logged in:", aurora.Cyan(newPID)) + return Session{}, false + } + + gpcmIPStr, _ := common.IPFormatToString(gpcmIP) + if gpcmIPStr != publicIP { + logging.Error(moduleName, "Caller public IP does not match GPCM session") + return Session{}, false + } + + // Constraint: only one session can exist with a profile ID + outdated := []uint32{} + for sessionID, otherSession := range sessions { + if otherPID, ok := otherSession.Data["dwc_pid"]; !ok || otherPID != newPID { + continue + } + + // Remove old sessions with the PID + outdated = append(outdated, sessionID) + } + + for _, sessionID := range outdated { + logging.Notice(moduleName, "Removing outdated session", aurora.BrightCyan(sessionID), "with PID", aurora.Cyan(newPID)) + delete(sessions, sessionID) + } + + logging.Notice(moduleName, "Opened session with PID", aurora.Cyan(newPID)) + } + } + + if !sessionExists { + logging.Notice(moduleName, "Creating session", aurora.Cyan(sessionId).String()) data := Session{ SessionID: sessionId, Challenge: "", @@ -49,35 +124,45 @@ func setSessionData(sessionId uint32, payload map[string]string) Session { } sessions[sessionId] = &data - return data + return data, true } session.Data = payload session.LastKeepAlive = time.Now().Unix() - - return *session + return *session, true } // Get a copy of the list of servers func GetSessionServers() []map[string]string { - var servers []map[string]string + servers := []map[string]string{} + unreachable := []uint32{} currentTime := time.Now().Unix() mutex.Lock() defer mutex.Unlock() for _, session := range sessions { + // If the last keep alive was over a minute ago then consider the server unreachable + if session.LastKeepAlive < currentTime-60 { + // If the last keep alive was over an hour ago then remove the server + if session.LastKeepAlive < currentTime-((60*60)*1) { + unreachable = append(unreachable, session.SessionID) + } + continue + } + if !session.Authenticated { continue } - // If the last keep alive was over a minute ago then consider the server unreachable - if session.LastKeepAlive < currentTime-60 { - continue - } - servers = append(servers, session.Data) } + // Remove unreachable sessions + for _, sessionID := range unreachable { + logging.Notice("QR2", "Removing unreachable session", aurora.BrightCyan(sessionID)) + delete(sessions, sessionID) + } + return servers } diff --git a/serverbrowser/filter.go b/serverbrowser/filter.go index 85c1ecf..bc1ce9a 100644 --- a/serverbrowser/filter.go +++ b/serverbrowser/filter.go @@ -37,10 +37,15 @@ func filterServers(servers []map[string]string, queryGame string, filter string, // If multiple servers exist with the same public IP then the client will use the one with the matching port. // This is a bit of a hack to speed up server creation. if _, ok := server["dwc_pid"]; !ok && server["publicip"] == publicIP { - server["dwc_pid"] = dwc_pid - server["dwc_mtype"] = "0" - server["dwc_mver"] = "0" - filtered = append(filtered, server) + // Create a copy of the map with some values changed + newServer := map[string]string{} + for k, v := range server { + newServer[k] = v + } + newServer["dwc_pid"] = dwc_pid + newServer["dwc_mtype"] = "0" + newServer["dwc_mver"] = "0" + filtered = append(filtered, newServer) } }