Add support for DWC match version 3

This commit is contained in:
mkwcat
2023-12-11 05:50:13 -05:00
parent 8693dff7fc
commit 66a5d0a778
4 changed files with 218 additions and 170 deletions

View File

@@ -73,6 +73,9 @@ type MatchCommandDataResvOK struct {
ReceiverNewAID uint32
ClientCount uint32
ResvCheckValue uint32
// Only exists in version 3
ProfileIDs []uint32
}
type MatchCommandDataResvDeny struct {
@@ -160,10 +163,14 @@ func GetMatchCommandString(command byte) string {
return "UNKNOWN"
}
func DecodeMatchCommand(command byte, buffer []byte) (MatchCommandData, bool) {
func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandData, bool) {
if version != 3 && version != 11 && version != 90 {
return MatchCommandData{}, false
}
switch command {
case MatchReservation:
if len(buffer) != 0x24 {
if (version == 3 && len(buffer) != 0xC) || (version == 90 && len(buffer) != 0x24) {
break
}
@@ -172,64 +179,102 @@ func DecodeMatchCommand(command byte, buffer []byte) (MatchCommandData, bool) {
break
}
isFriendValue := binary.LittleEndian.Uint32(buffer[0x18:0x1C])
if isFriendValue > 1 {
break
}
isFriend := isFriendValue != 0
publicPort := binary.LittleEndian.Uint32(buffer[0x08:0x0C])
if publicPort > 0xffff {
break
}
localPort := binary.LittleEndian.Uint32(buffer[0x10:0x14])
if localPort > 0xffff {
break
}
if version == 3 {
return MatchCommandData{Reservation: &MatchCommandDataReservation{
MatchType: byte(matchType),
PublicIP: binary.BigEndian.Uint32(buffer[0x04:0x08]),
PublicPort: uint16(publicPort),
}}, true
} else if version == 90 {
localPort := binary.LittleEndian.Uint32(buffer[0x10:0x14])
if localPort > 0xffff {
break
}
return MatchCommandData{Reservation: &MatchCommandDataReservation{
MatchType: byte(matchType),
PublicIP: binary.BigEndian.Uint32(buffer[0x04:0x08]),
PublicPort: uint16(publicPort),
LocalIP: binary.BigEndian.Uint32(buffer[0x0C:0x10]),
LocalPort: uint16(localPort),
Unknown: binary.LittleEndian.Uint32(buffer[0x14:0x18]),
IsFriend: isFriend,
LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x1C:0x20]),
ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x20:0x24]),
}}, true
isFriendValue := binary.LittleEndian.Uint32(buffer[0x18:0x1C])
if isFriendValue > 1 {
break
}
isFriend := isFriendValue != 0
return MatchCommandData{Reservation: &MatchCommandDataReservation{
MatchType: byte(matchType),
PublicIP: binary.BigEndian.Uint32(buffer[0x04:0x08]),
PublicPort: uint16(publicPort),
LocalIP: binary.BigEndian.Uint32(buffer[0x0C:0x10]),
LocalPort: uint16(localPort),
Unknown: binary.LittleEndian.Uint32(buffer[0x14:0x18]),
IsFriend: isFriend,
LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x1C:0x20]),
ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x20:0x24]),
}}, true
}
case MatchResvOK:
if len(buffer) != 0x34 {
break
}
switch version {
case 3:
if len(buffer) < 0xC {
break
}
publicPort := binary.LittleEndian.Uint32(buffer[0x10:0x14])
if publicPort > 0xffff {
break
}
clientCount := binary.LittleEndian.Uint32(buffer[0x00:0x04])
if clientCount > 29 || len(buffer) != int(0xC+clientCount*0x4) {
break
}
localPort := binary.LittleEndian.Uint32(buffer[0x18:0x1C])
if localPort > 0xffff {
break
}
var profileIDs []uint32
for i := uint32(0); i < clientCount; i++ {
profileIDs = append(profileIDs, binary.LittleEndian.Uint32(buffer[0x4+i*4:0x4+i*4+4]))
}
return MatchCommandData{ResvOK: &MatchCommandDataResvOK{
MaxPlayers: binary.LittleEndian.Uint32(buffer[0x00:0x04]),
SenderAID: binary.LittleEndian.Uint32(buffer[0x04:0x08]),
ProfileID: binary.LittleEndian.Uint32(buffer[0x08:0x0C]),
PublicIP: binary.BigEndian.Uint32(buffer[0x0C:0x10]),
PublicPort: uint16(publicPort),
LocalIP: binary.BigEndian.Uint32(buffer[0x14:0x18]),
LocalPort: uint16(localPort),
Unknown: binary.LittleEndian.Uint32(buffer[0x1C:0x20]),
LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x20:0x24]),
GroupID: binary.LittleEndian.Uint32(buffer[0x24:0x28]),
ReceiverNewAID: binary.LittleEndian.Uint32(buffer[0x28:0x2C]),
ClientCount: binary.LittleEndian.Uint32(buffer[0x2C:0x30]),
ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x30:0x34]),
}}, true
publicPort := binary.LittleEndian.Uint32(buffer[0x08:0x0C])
if publicPort > 0xffff {
break
}
return MatchCommandData{ResvOK: &MatchCommandDataResvOK{
PublicIP: binary.BigEndian.Uint32(buffer[0x04:0x08]),
PublicPort: uint16(publicPort),
ClientCount: clientCount,
ProfileIDs: profileIDs,
}}, true
case 90:
if len(buffer) != 0x34 {
break
}
publicPort := binary.LittleEndian.Uint32(buffer[0x10:0x14])
if publicPort > 0xffff {
break
}
localPort := binary.LittleEndian.Uint32(buffer[0x18:0x1C])
if localPort > 0xffff {
break
}
return MatchCommandData{ResvOK: &MatchCommandDataResvOK{
MaxPlayers: binary.LittleEndian.Uint32(buffer[0x00:0x04]),
SenderAID: binary.LittleEndian.Uint32(buffer[0x04:0x08]),
ProfileID: binary.LittleEndian.Uint32(buffer[0x08:0x0C]),
PublicIP: binary.BigEndian.Uint32(buffer[0x0C:0x10]),
PublicPort: uint16(publicPort),
LocalIP: binary.BigEndian.Uint32(buffer[0x14:0x18]),
LocalPort: uint16(localPort),
Unknown: binary.LittleEndian.Uint32(buffer[0x1C:0x20]),
LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x20:0x24]),
GroupID: binary.LittleEndian.Uint32(buffer[0x24:0x28]),
ReceiverNewAID: binary.LittleEndian.Uint32(buffer[0x28:0x2C]),
ClientCount: binary.LittleEndian.Uint32(buffer[0x2C:0x30]),
ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x30:0x34]),
}}, true
}
case MatchResvDeny:
if len(buffer) != 0x04 {
@@ -354,41 +399,64 @@ func DecodeMatchCommand(command byte, buffer []byte) (MatchCommandData, bool) {
return MatchCommandData{}, false
}
func EncodeMatchCommand(command byte, data MatchCommandData) ([]byte, bool) {
func EncodeMatchCommand(command byte, data MatchCommandData, version int) ([]byte, bool) {
if version != 3 && version != 11 && version != 90 {
return []byte{}, false
}
switch command {
case MatchReservation:
message := binary.LittleEndian.AppendUint32([]byte{}, uint32(data.Reservation.MatchType))
message = binary.BigEndian.AppendUint32(message, data.Reservation.PublicIP)
message = binary.LittleEndian.AppendUint32(message, uint32(data.Reservation.PublicPort))
message = binary.BigEndian.AppendUint32(message, data.Reservation.LocalIP)
message = binary.LittleEndian.AppendUint32(message, uint32(data.Reservation.LocalPort))
message = binary.LittleEndian.AppendUint32(message, data.Reservation.Unknown)
isFriendInt := uint32(0)
if data.Reservation.IsFriend {
isFriendInt = 1
if version != 3 {
message = binary.BigEndian.AppendUint32(message, data.Reservation.LocalIP)
message = binary.LittleEndian.AppendUint32(message, uint32(data.Reservation.LocalPort))
message = binary.LittleEndian.AppendUint32(message, data.Reservation.Unknown)
isFriendInt := uint32(0)
if data.Reservation.IsFriend {
isFriendInt = 1
}
message = binary.LittleEndian.AppendUint32(message, isFriendInt)
message = binary.LittleEndian.AppendUint32(message, data.Reservation.LocalPlayerCount)
message = binary.LittleEndian.AppendUint32(message, data.Reservation.ResvCheckValue)
}
message = binary.LittleEndian.AppendUint32(message, isFriendInt)
message = binary.LittleEndian.AppendUint32(message, data.Reservation.LocalPlayerCount)
message = binary.LittleEndian.AppendUint32(message, data.Reservation.ResvCheckValue)
return message, true
case MatchResvOK:
message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvOK.MaxPlayers)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.SenderAID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ProfileID)
message = binary.BigEndian.AppendUint32(message, data.ResvOK.PublicIP)
message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.PublicPort))
message = binary.BigEndian.AppendUint32(message, data.ResvOK.LocalIP)
message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.LocalPort))
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.Unknown)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.LocalPlayerCount)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.GroupID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ReceiverNewAID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ClientCount)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ResvCheckValue)
return message, true
switch version {
case 3:
if int(data.ResvOK.ClientCount) != len(data.ResvOK.ProfileIDs) {
return []byte{}, false
}
message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvOK.ClientCount)
for _, pid := range data.ResvOK.ProfileIDs {
message = binary.LittleEndian.AppendUint32(message, pid)
}
message = binary.BigEndian.AppendUint32(message, data.ResvOK.PublicIP)
message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.PublicPort))
return message, true
case 90:
message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvOK.MaxPlayers)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.SenderAID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ProfileID)
message = binary.BigEndian.AppendUint32(message, data.ResvOK.PublicIP)
message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.PublicPort))
message = binary.BigEndian.AppendUint32(message, data.ResvOK.LocalIP)
message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.LocalPort))
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.Unknown)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.LocalPlayerCount)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.GroupID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ReceiverNewAID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ClientCount)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ResvCheckValue)
return message, true
}
case MatchResvDeny:
message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvDeny.Reason)

View File

@@ -1,6 +1,7 @@
package gpcm
import (
"encoding/binary"
"github.com/logrusorgru/aurora/v3"
"strconv"
"strings"
@@ -161,40 +162,60 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
}
// Parse message for security and room tracking purposes
if !strings.HasPrefix(msg, "GPCM") {
logging.Error(g.ModuleName, "Invalid message prefix")
var version int
var msgDataIndex int
if strings.HasPrefix(msg, "GPCM3vMAT") {
version = 3
msgDataIndex = 9
} else if strings.HasPrefix(msg, "GPCM11vMAT") {
// Only used for Brawl
version = 11
msgDataIndex = 10
} else if strings.HasPrefix(msg, "GPCM90vMAT") {
version = 90
msgDataIndex = 10
} else {
logging.Error(g.ModuleName, "Invalid message prefix; message:", msg)
g.replyError(ErrMessage)
return
}
currentIndex := strings.Index(msg, "vMAT") + 4
isMessageHeaderValid := false
switch currentIndex {
case 9: // 1 - 9
isMessageHeaderValid = msg[4] >= '1' && msg[4] <= '9' && len(msg) >= 11
case 10: // 10 - 99
isMessageHeaderValid = msg[4] >= '1' && msg[4] <= '9' && msg[5] >= '0' && msg[5] <= '9' && len(msg) >= 12
}
if !isMessageHeaderValid {
logging.Error(g.ModuleName, "Invalid message header")
g.replyError(ErrMessage)
return
cmd := msg[msgDataIndex]
msgDataIndex++
var msgData []byte
switch version {
case 3:
for _, stringValue := range strings.Split(msg[msgDataIndex:], "/") {
intValue, err := strconv.ParseUint(stringValue, 10, 32)
if err != nil {
logging.Error(g.ModuleName, "Invalid message value; message:", msg)
g.replyError(ErrMessage)
return
}
msgData = binary.LittleEndian.AppendUint32(msgData, uint32(intValue))
}
break
// TODO: Version 11 (Super Smash Bros. Brawl)
case 90:
msgData, err = common.Base64DwcEncoding.DecodeString(msg[msgDataIndex:])
if err != nil {
logging.Error(g.ModuleName, "Invalid message base64 data; message:", msg)
g.replyError(ErrMessage)
return
}
break
}
cmd := msg[currentIndex]
currentIndex++
msgData, err := common.Base64DwcEncoding.DecodeString(msg[currentIndex:])
if err != nil {
logging.Error(g.ModuleName, "Invalid message base64 data")
g.replyError(ErrMessage)
return
}
msgMatchData, ok := common.DecodeMatchCommand(cmd, msgData)
msgMatchData, ok := common.DecodeMatchCommand(cmd, msgData, version)
common.LogMatchCommand(g.ModuleName, strconv.FormatInt(int64(toProfileId), 10), cmd, msgMatchData)
if !ok {
logging.Error(g.ModuleName, "Invalid match command data")
logging.Error(g.ModuleName, "Invalid match command data; message:", msg)
g.replyError(ErrMessage)
return
}

View File

@@ -54,6 +54,12 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
return
}
version := int(binary.LittleEndian.Uint32(message[0x04:0x08]))
if version != 3 && version != 11 && version != 90 {
logging.Error(moduleName, "Received invalid match version")
return
}
senderProfileID := binary.LittleEndian.Uint32(message[0x10:0x14])
moduleName = "QR2/MSG:p" + strconv.FormatUint(uint64(senderProfileID), 10)
@@ -81,7 +87,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
}
var ok bool
matchData, ok = common.DecodeMatchCommand(message[8], message[0x14:])
matchData, ok = common.DecodeMatchCommand(message[8], message[0x14:], version)
if !ok {
logging.Error(moduleName, "Received invalid match command:", aurora.Cyan(message[8]))
return
@@ -99,14 +105,16 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
return
}
if matchData.Reservation.PublicPort < 1024 {
logging.Error(moduleName, "RESERVATION: Public port is reserved")
if qr2Port != matchData.Reservation.PublicPort {
logging.Error(moduleName, "RESERVATION: Public port mismatch in header and command")
return
}
if matchData.Reservation.LocalPort < 1024 {
logging.Error(moduleName, "RESERVATION: Local port is reserved")
return
if version != 3 {
if matchData.Reservation.LocalPort < 1024 {
logging.Error(moduleName, "RESERVATION: Local port is reserved")
return
}
}
if useSearchID {
@@ -123,19 +131,21 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
return
}
if matchData.ResvOK.PublicPort < 1024 {
logging.Error(moduleName, "RESV_OK: Public port is reserved")
if qr2Port != matchData.Reservation.PublicPort {
logging.Error(moduleName, "RESERVATION: Public port mismatch in header and command")
return
}
if matchData.ResvOK.LocalPort < 1024 {
logging.Error(moduleName, "RESV_OK: Local port is reserved")
return
}
if version != 3 {
if matchData.ResvOK.LocalPort < 1024 {
logging.Error(moduleName, "RESV_OK: Local port is reserved")
return
}
if matchData.ResvOK.ProfileID != senderProfileID {
logging.Error(moduleName, "RESV_OK: Profile ID mismatch in header")
return
if matchData.ResvOK.ProfileID != senderProfileID {
logging.Error(moduleName, "RESV_OK: Profile ID mismatch in header")
return
}
}
if useSearchID {
@@ -171,7 +181,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
}
var matchMessage []byte
matchMessage, ok = common.EncodeMatchCommand(message[8], matchData)
matchMessage, ok = common.EncodeMatchCommand(message[8], matchData, version)
if !ok {
logging.Error(moduleName, "Failed to reencode match command:", aurora.Cyan(message[8]))
return

View File

@@ -15,53 +15,6 @@ import (
// Example: dwc_mver = 90 and dwc_pid != 43 and maxplayers = 11 and numplayers < 11 and dwc_mtype = 0 and dwc_hoststate = 2 and dwc_suspend = 0 and (rk = 'vs' and ev >= 4250 and ev <= 5750 and p = 0)
func filterServers(servers []map[string]string, queryGame string, expression string, publicIP string) []map[string]string {
if match := regexSelfLookup.FindStringSubmatch(expression); match != nil {
dwcPid := match[1]
var filtered []map[string]string
// Search for where the profile ID matches
for _, server := range servers {
if server["gamename"] != queryGame {
continue
}
if server["dwc_pid"] == dwcPid {
if server["publicip"] != publicIP {
logging.Error(ModuleName, "Self lookup", aurora.Cyan(dwcPid), "from wrong IP")
return []map[string]string{}
}
logging.Info(ModuleName, "Self lookup from", aurora.Cyan(dwcPid), "ok")
filtered = []map[string]string{server}
break
}
// Alternatively, if the server hasn't set its dwcPid field yet, we return servers matching the request's public IP.
// 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 {
// 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"] = dwcPid
newServer["dwc_mtype"] = "0"
newServer["dwc_mver"] = "0"
filtered = append(filtered, newServer)
}
}
if len(filtered) == 0 {
logging.Error(ModuleName, "Could not find server with dwcPid", aurora.Cyan(dwcPid))
return []map[string]string{}
}
logging.Info(ModuleName, "Self lookup for", aurora.Cyan(dwcPid), "matched", aurora.BrightCyan(len(filtered)), "servers via public IP")
return filtered
}
// Matchmaking search
tree, err := filter.Parse(expression)
if err != nil {
@@ -76,7 +29,7 @@ func filterServers(servers []map[string]string, queryGame string, expression str
continue
}
if server["dwc_hoststate"] != "0" && server["dwc_hoststate"] != "2" {
if server["dwc_mver"] == "90" && (server["dwc_hoststate"] != "0" && server["dwc_hoststate"] != "2") {
continue
}
@@ -105,12 +58,8 @@ func filterSelfLookup(servers []map[string]string, queryGame string, dwcPid stri
}
if server["dwc_pid"] == dwcPid {
if server["publicip"] != publicIP {
logging.Error(ModuleName, "Self lookup", aurora.Cyan(dwcPid), "from wrong IP")
return []map[string]string{}
}
logging.Info(ModuleName, "Self lookup from", aurora.Cyan(dwcPid), "ok")
// May not be a self lookup, some games search for friends like this
logging.Info(ModuleName, "Lookup", aurora.Cyan(dwcPid), "ok")
return []map[string]string{server}
}