QR2: Verify profile ID and public IP

This commit is contained in:
mkwcat
2023-11-01 10:12:47 -04:00
parent 78e0afff7c
commit 85b917951d
5 changed files with 143 additions and 36 deletions

View File

@@ -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 ""
}

View File

@@ -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)

View File

@@ -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:

View File

@@ -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
}

View File

@@ -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)
}
}