diff --git a/common/match_command.go b/common/match_command.go index b0e9b0c..53d5cd4 100644 --- a/common/match_command.go +++ b/common/match_command.go @@ -354,6 +354,92 @@ func DecodeMatchCommand(command byte, buffer []byte) (MatchCommandData, bool) { return MatchCommandData{}, false } +func EncodeMatchCommand(command byte, data MatchCommandData) ([]byte, bool) { + 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 + } + 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 + + case MatchResvDeny: + message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvDeny.Reason) + return message, true + + case MatchResvWait: + return []byte{}, true + + case MatchResvCancel: + return []byte{}, true + + case MatchTellAddr: + message := binary.BigEndian.AppendUint32([]byte{}, data.TellAddr.LocalIP) + message = binary.LittleEndian.AppendUint32(message, uint32(data.TellAddr.LocalPort)) + return message, true + + case MatchServerCloseClient: + message := []byte{} + for i := 0; i < len(data.ServerCloseClient.ProfileIDs); i++ { + message = binary.LittleEndian.AppendUint32(message, data.ServerCloseClient.ProfileIDs[i]) + } + return message, true + + case MatchSuspendMatch: + message := binary.LittleEndian.AppendUint32([]byte{}, data.SuspendMatch.HostProfileID) + + isHostInt := uint32(0) + if data.SuspendMatch.IsHost { + isHostInt = 1 + } + message = binary.LittleEndian.AppendUint32(message, isHostInt) + + if data.SuspendMatch.SuspendValue != nil { + suspendValueInt := uint32(0) + if *data.SuspendMatch.SuspendValue { + suspendValueInt = 1 + } + message = binary.LittleEndian.AppendUint32(message, suspendValueInt) + + if data.SuspendMatch.ClientAID != nil { + message = binary.LittleEndian.AppendUint32(message, *data.SuspendMatch.ClientAID) + } else if data.SuspendMatch.ClientAIDUsageMask != nil { + message = binary.LittleEndian.AppendUint32(message, *data.SuspendMatch.ClientAIDUsageMask) + } + } + return message, true + } + + return []byte{}, false +} + func LogMatchCommand(moduleName string, dest string, command byte, data MatchCommandData) { logging.Notice(moduleName, "Match", aurora.Yellow(GetMatchCommandString(command)), "to", aurora.BrightCyan(dest)) diff --git a/qr2/heartbeat.go b/qr2/heartbeat.go index 5d5abc2..1fa2d93 100644 --- a/qr2/heartbeat.go +++ b/qr2/heartbeat.go @@ -19,14 +19,28 @@ func heartbeat(conn net.PacketConn, addr net.Addr, buffer []byte) { payload := map[string]string{} for i := 0; i < len(values); i += 2 { - if values[i] == "" { - break + if len(values[i]) == 0 || values[i][0] == '+' { + continue } payload[values[i]] = values[i+1] logging.Info(moduleName, aurora.Cyan(values[i]).String()+":", aurora.Cyan(values[i+1])) } + realIP, realPort := common.IPFormatToString(addr.String()) + + if ip, ok := payload["publicip"]; !ok || ip == "0" { + // Set the public IP key to the real IP + payload["publicip"] = realIP + payload["publicport"] = realPort + } + + // Client is mistaken about its public IP + if payload["publicip"] != realIP || payload["publicport"] != realPort { + logging.Error(moduleName, "Public IP mismatch") + return + } + if statechanged, ok := payload["statechanged"]; ok { if statechanged == "1" { // TODO: This would be a good place to run the server->client message exploit @@ -43,21 +57,12 @@ func heartbeat(conn net.PacketConn, addr net.Addr, buffer []byte) { } } - realIP, realPort := common.IPFormatToString(addr.String()) - - publicIPKey, hasPublicIPKey := payload["publicip"] - if !hasPublicIPKey || publicIPKey != realIP { - // Set the public IP key to the real IP, and then send the challenge (done later) - payload["publicip"] = realIP - payload["publicport"] = realPort - } - - session, ok := setSessionData(sessionId, payload) + session, ok := setSessionData(sessionId, payload, addr) if !ok { return } - if !session.Authenticated || !hasPublicIPKey || publicIPKey != realIP { + if !session.Authenticated { logging.Notice(moduleName, "Sending challenge") sendChallenge(conn, addr, session) return diff --git a/qr2/main.go b/qr2/main.go index 4425300..21d8614 100644 --- a/qr2/main.go +++ b/qr2/main.go @@ -5,19 +5,11 @@ import ( "github.com/logrusorgru/aurora/v3" "net" "strconv" - "sync" "time" "wwfc/common" "wwfc/logging" ) -var ( - // I would use a sync.Map instead of the map mutex combo, but this performs better. - sessions = map[uint32]*Session{} - mutex = sync.RWMutex{} - masterConn net.PacketConn -) - const ( QueryRequest = 0x00 ChallengeRequest = 0x01 @@ -32,6 +24,8 @@ const ( ClientRegisteredReply = 0x0A ) +var masterConn net.PacketConn + func StartServer() { // Get config config := common.GetConfig() diff --git a/qr2/session.go b/qr2/session.go index 07e67a8..fbedc88 100644 --- a/qr2/session.go +++ b/qr2/session.go @@ -4,8 +4,10 @@ import ( "bytes" "encoding/binary" "github.com/logrusorgru/aurora/v3" + "math/rand" "net" "strconv" + "sync" "time" "wwfc/common" "wwfc/gpcm" @@ -20,6 +22,7 @@ const ( type Session struct { SessionID uint32 + SearchID uint64 Addr net.Addr Challenge string Authenticated bool @@ -29,13 +32,19 @@ type Session struct { PacketCount uint32 } +var ( + // I would use a sync.Map instead of the map mutex combo, but this performs better. + sessions = map[uint32]*Session{} + mutex = sync.RWMutex{} +) + // Remove a session. func removeSession(sessionId uint32) { delete(sessions, sessionId) } // 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, bool) { +func setSessionData(sessionId uint32, payload map[string]string, addr net.Addr) (Session, bool) { moduleName := "QR2:" + strconv.FormatInt(int64(sessionId), 10) // Perform sanity checks on the session data. This is a mess but @@ -55,6 +64,11 @@ func setSessionData(sessionId uint32, payload map[string]string) (Session, bool) defer mutex.Unlock() session, sessionExists := sessions[sessionId] + if sessionExists && session.Data["publicip"] != publicIP { + logging.Error(moduleName, "Public IP mismatch") + return Session{}, false + } + if newPIDValid { var oldPID string oldPIDValid := false @@ -117,6 +131,8 @@ func setSessionData(sessionId uint32, payload map[string]string) (Session, bool) logging.Notice(moduleName, "Creating session", aurora.Cyan(sessionId).String()) data := Session{ SessionID: sessionId, + SearchID: uint64(rand.Int63n(0x400 << 32)), + Addr: addr, Challenge: "", Authenticated: false, LastKeepAlive: time.Now().Unix(), @@ -124,11 +140,13 @@ func setSessionData(sessionId uint32, payload map[string]string) (Session, bool) Data: payload, PacketCount: 0, } - sessions[sessionId] = &data + data.Data["+searchid"] = strconv.FormatUint(data.SearchID, 10) + sessions[sessionId] = &data return data, true } + payload["+searchid"] = session.Data["+searchid"] session.Data = payload session.LastKeepAlive = time.Now().Unix() return *session, true @@ -168,11 +186,79 @@ func GetSessionServers() []map[string]string { return servers } -func SendClientMessage(destIP string, message []byte) { +func getSessionByPublicIP(publicIP uint32, publicPort uint16) *Session { + ipStr := strconv.FormatInt(int64(int32(publicIP)), 10) + portStr := strconv.FormatUint(uint64(publicPort), 10) + + currentTime := time.Now().Unix() + mutex.Lock() + defer mutex.Unlock() + + // Find the session with the IP + for _, session := range sessions { + 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 + } + + if session.Data["publicip"] == ipStr && session.Data["publicport"] == portStr { + return session + } + } + + return nil +} + +func getSessionBySearchID(searchID uint64) *Session { + currentTime := time.Now().Unix() + mutex.Lock() + defer mutex.Unlock() + + // Find the session with the ID + for _, session := range sessions { + 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 + } + + if session.SearchID == searchID { + return session + } + } + + return nil +} + +func SendClientMessage(senderIP string, destSearchID uint64, message []byte) { moduleName := "QR2/MSG" var matchData common.MatchCommandData - senderProfileID := uint32(0) + var sender *Session + var receiver *Session + senderIPInt, _ := common.IPFormatToInt(senderIP) + + useSearchID := destSearchID < (0x400 << 32) + if useSearchID { + receiver = getSessionBySearchID(destSearchID) + } else { + // It's an IP address, used in some circumstances + receiverIPInt := uint32(destSearchID & 0xffffffff) + receiverPort := uint16(destSearchID >> 32) + receiver = getSessionByPublicIP(receiverIPInt, receiverPort) + } + + if receiver == nil { + logging.Error(moduleName, "Destination", aurora.Cyan(destSearchID), "does not exist") + return + } // Decode and validate the message isNatnegPacket := false @@ -186,7 +272,6 @@ func SendClientMessage(destIP string, message []byte) { senderSessionID := binary.LittleEndian.Uint32(message[0x6:0xA]) moduleName = "QR2/MSG:s" + strconv.FormatUint(uint64(senderSessionID), 10) - } else if bytes.Equal(message[:4], []byte{0xbb, 0x49, 0xcc, 0x4d}) { // DWC match command if len(message) < 0x14 { @@ -194,7 +279,7 @@ func SendClientMessage(destIP string, message []byte) { return } - senderProfileID = binary.LittleEndian.Uint32(message[0x10:0x14]) + senderProfileID := binary.LittleEndian.Uint32(message[0x10:0x14]) moduleName = "QR2/MSG:p" + strconv.FormatUint(uint64(senderProfileID), 10) if (int(message[9]) + 0x14) != len(message) { @@ -202,6 +287,19 @@ func SendClientMessage(destIP string, message []byte) { return } + qr2IP := binary.BigEndian.Uint32(message[0x0C:0x10]) + qr2Port := binary.LittleEndian.Uint16(message[0x0A:0x0C]) + + if senderIPInt != int32(qr2IP) { + logging.Error(moduleName, "Wrong QR2 IP in match command packet header") + } + + sender = getSessionByPublicIP(qr2IP, qr2Port) + if sender == nil { + logging.Error(moduleName, "Session does not exist with QR2 IP and port") + return + } + var ok bool matchData, ok = common.DecodeMatchCommand(message[8], message[0x14:]) if !ok { @@ -216,10 +314,9 @@ func SendClientMessage(destIP string, message []byte) { return } - if common.IsReservedIP(int32(matchData.Reservation.PublicIP)) { - logging.Warn(moduleName, "RESERVATION: Public IP is reserved") - // Temporarily disabled for localhost testing - // TODO: Add a config option or something + if qr2IP != matchData.Reservation.PublicIP { + logging.Error(moduleName, "RESERVATION: Public IP mismatch in header and command") + return } if matchData.Reservation.PublicPort < 1024 { @@ -231,12 +328,19 @@ func SendClientMessage(destIP string, message []byte) { logging.Error(moduleName, "RESERVATION: Local port is reserved") return } + + if useSearchID { + matchData.Reservation.PublicIP = uint32(sender.SearchID & 0xffffffff) + matchData.Reservation.PublicPort = uint16((sender.SearchID >> 32) & 0xffff) + matchData.Reservation.LocalIP = 0 + matchData.Reservation.LocalPort = 0 + } } if message[8] == common.MatchResvOK { - if common.IsReservedIP(int32(matchData.ResvOK.PublicIP)) { - logging.Warn(moduleName, "RESV_OK: Public IP is reserved") - // TODO: See above + if qr2IP != matchData.ResvOK.PublicIP { + logging.Error(moduleName, "RESERVATION: Public IP mismatch in header and command") + return } if matchData.ResvOK.PublicPort < 1024 { @@ -253,69 +357,81 @@ func SendClientMessage(destIP string, message []byte) { logging.Error(moduleName, "RESV_OK: Profile ID mismatch in header") return } + + if useSearchID { + matchData.ResvOK.PublicIP = uint32(sender.SearchID & 0xffffffff) + matchData.ResvOK.PublicPort = uint16((sender.SearchID >> 32) & 0xffff) + matchData.ResvOK.LocalIP = 0 + matchData.ResvOK.LocalPort = 0 + } } if message[8] == common.MatchTellAddr { - // TODO: Check if the public IPs are actually the same + mutex.Lock() + if sender.Data["publicip"] != receiver.Data["publicip"] { + mutex.Unlock() + logging.Error(moduleName, "TELL_ADDR: Public IP does not match receiver") + return + } + mutex.Unlock() + if matchData.TellAddr.LocalPort < 1024 { logging.Error(moduleName, "TELL_ADDR: Local port is reserved") return } } - } - destIPIntStr, destPortStr := common.IPFormatToString(destIP) - - currentTime := time.Now().Unix() - mutex.Lock() - // Find the session with the IP - for _, session := range sessions { - if !session.Authenticated { - continue + if useSearchID { + // Convert public IP to search ID + qr2SearchID := binary.LittleEndian.AppendUint16([]byte{}, uint16((sender.SearchID>>32)&0xffff)) + qr2SearchID = binary.BigEndian.AppendUint32(qr2SearchID, uint32(sender.SearchID&0xffffffff)) + message = append(message[:0x0A], append(qr2SearchID, message[0x10:0x14]...)...) + } else { + message = message[:0x14] } - // If the last keep alive was over a minute ago then consider the server unreachable - if session.LastKeepAlive < currentTime-60 { - continue - } - - if session.Data["publicip"] == destIPIntStr && session.Data["publicport"] == destPortStr { - destPid, ok := session.Data["dwc_pid"] - if !ok || destPid == "" { - destPid = "" - } - - destSessionID := session.SessionID - packetCount := session.PacketCount + 1 - session.PacketCount = packetCount - - mutex.Unlock() - - if isNatnegPacket { - cookie := binary.BigEndian.Uint32(message[0x2:0x6]) - logging.Notice(moduleName, "Send NN cookie", aurora.Cyan(cookie), "to", aurora.BrightCyan(destPid)) - } else { - common.LogMatchCommand(moduleName, destPid, message[8], matchData) - } - - // Found the client, now send the message - payload := createResponseHeader(ClientMessageRequest, destSessionID) - - payload = append(payload, []byte{0, 0, 0, 0}...) - binary.BigEndian.PutUint32(payload[len(payload)-4:], packetCount) - payload = append(payload, message...) - - destIPAddr, err := net.ResolveUDPAddr("udp", destIP) - if err != nil { - panic(err) - } - - // TODO: Send again if no CLIENT_MESSAGE_ACK is received after - masterConn.WriteTo(payload, destIPAddr) + var matchMessage []byte + matchMessage, ok = common.EncodeMatchCommand(message[8], matchData) + if !ok { + logging.Error(moduleName, "Failed to reencode match command:", aurora.Cyan(message[8])) return } + + if len(matchMessage) != 0 { + message = append(message, matchMessage...) + } } + + mutex.Lock() + + destPid, ok := receiver.Data["dwc_pid"] + if !ok || destPid == "" { + destPid = "" + } + + destSessionID := receiver.SessionID + packetCount := receiver.PacketCount + 1 + receiver.PacketCount = packetCount + destAddr := receiver.Addr + mutex.Unlock() - logging.Error(moduleName, "Could not find destination server") + if isNatnegPacket { + cookie := binary.BigEndian.Uint32(message[0x2:0x6]) + logging.Notice(moduleName, "Send NN cookie", aurora.Cyan(cookie), "to", aurora.BrightCyan(destPid)) + } else { + common.LogMatchCommand(moduleName, destPid, message[8], matchData) + } + + payload := createResponseHeader(ClientMessageRequest, destSessionID) + + payload = append(payload, []byte{0, 0, 0, 0}...) + binary.BigEndian.PutUint32(payload[len(payload)-4:], packetCount) + payload = append(payload, message...) + + // TODO: Send again if no CLIENT_MESSAGE_ACK is received after + _, err := masterConn.WriteTo(payload, destAddr) + if err != nil { + logging.Error(moduleName, "Error sending message:", err.Error()) + } } diff --git a/serverbrowser/filter.go b/serverbrowser/filter.go index a258afc..4ce9586 100644 --- a/serverbrowser/filter.go +++ b/serverbrowser/filter.go @@ -2,7 +2,6 @@ package serverbrowser import ( "github.com/logrusorgru/aurora/v3" - "regexp" "wwfc/logging" "wwfc/serverbrowser/filter" ) @@ -15,8 +14,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) -var regexSelfLookup = regexp.MustCompile(`^dwc_pid ?= ?(\d{1,10})$`) - func filterServers(servers []map[string]string, queryGame string, expression string, publicIP string) []map[string]string { if match := regexSelfLookup.FindStringSubmatch(expression); match != nil { dwc_pid := match[1] @@ -36,7 +33,8 @@ func filterServers(servers []map[string]string, queryGame string, expression str } logging.Info(ModuleName, "Self lookup from", aurora.Cyan(dwc_pid), "ok") - return []map[string]string{server} + filtered = []map[string]string{server} + break } // Alternatively, if the server hasn't set its dwc_pid field yet, we return servers matching the request's public IP. @@ -96,3 +94,47 @@ func filterServers(servers []map[string]string, queryGame string, expression str logging.Info(ModuleName, "Matched", aurora.BrightCyan(len(filtered)), "servers") return filtered } + +func filterSelfLookup(servers []map[string]string, queryGame string, dwc_pid string, publicIP string) []map[string]string { + filtered := []map[string]string{} + + // Search for where the profile ID matches + for _, server := range servers { + if server["gamename"] != queryGame { + continue + } + + if server["dwc_pid"] == dwc_pid { + if server["publicip"] != publicIP { + logging.Error(ModuleName, "Self lookup", aurora.Cyan(dwc_pid), "from wrong IP") + return []map[string]string{} + } + + logging.Info(ModuleName, "Self lookup from", aurora.Cyan(dwc_pid), "ok") + return []map[string]string{server} + } + + // Alternatively, if the server hasn't set its dwc_pid 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"] = dwc_pid + newServer["dwc_mtype"] = "0" + newServer["dwc_mver"] = "0" + filtered = append(filtered, newServer) + } + } + + if len(filtered) == 0 { + logging.Error(ModuleName, "Could not find server with dwc_pid", aurora.Cyan(dwc_pid)) + return []map[string]string{} + } + + logging.Info(ModuleName, "Self lookup for", aurora.Cyan(dwc_pid), "matched", aurora.BrightCyan(len(filtered)), "servers via public IP") + return filtered +} diff --git a/serverbrowser/server.go b/serverbrowser/server.go index 6225fe4..0df413c 100644 --- a/serverbrowser/server.go +++ b/serverbrowser/server.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/logrusorgru/aurora/v3" "net" + "regexp" "strconv" "strings" "wwfc/common" @@ -51,6 +52,8 @@ func popUint32(buffer []byte, index int) (uint32, int) { return binary.BigEndian.Uint32(buffer[index:]), index + 4 } +var regexSelfLookup = regexp.MustCompile(`^dwc_pid ?= ?(\d{1,10})$`) + func handleServerListRequest(conn net.Conn, buffer []byte) { index := 9 queryGame, index := popString(buffer, index) @@ -84,6 +87,11 @@ func handleServerListRequest(conn net.Conn, buffer []byte) { continue } + // Skip private fields + if field == "publicip" || field == "publicport" || strings.HasPrefix(field, "localip") || field == "localport" { + continue + } + fieldList = append(fieldList, field) } @@ -116,8 +124,15 @@ func handleServerListRequest(conn net.Conn, buffer []byte) { } output = append(output, 0x00) // Zero length string to end the list - publicIP, _ := common.IPFormatToString(conn.RemoteAddr().String()) - servers := filterServers(qr2.GetSessionServers(), queryGame, filter, publicIP) + callerPublicIP, _ := common.IPFormatToString(conn.RemoteAddr().String()) + + var servers []map[string]string + if match := regexSelfLookup.FindStringSubmatch(filter); match != nil { + // Self lookup is handled differently + servers = filterSelfLookup(qr2.GetSessionServers(), queryGame, match[1], callerPublicIP) + } else { + servers = filterServers(qr2.GetSessionServers(), queryGame, filter, callerPublicIP) + } for _, server := range servers { var flags byte @@ -138,82 +153,107 @@ func handleServerListRequest(conn net.Conn, buffer []byte) { continue } - ip, err := strconv.ParseInt(publicip, 10, 32) - if err != nil { - logging.Error(ModuleName, "Server has invalid public IP value:", aurora.Cyan(publicip)) - } - - flagsBuffer = binary.BigEndian.AppendUint32(flagsBuffer, uint32(ip)) - - var port string - port, exists = server["publicport"] - if !exists { - // Fall back to local port if public port doesn't exist - if port, exists = server["localport"]; !exists { - logging.Error(ModuleName, "Server exists without port (publicip =", aurora.Cyan(publicip).String()+")") - continue + if publicip == callerPublicIP { + // Use the real public IP if it matches the caller's + ip, err := strconv.ParseInt(publicip, 10, 32) + if err != nil { + logging.Error(ModuleName, "Server has invalid public IP value:", aurora.Cyan(publicip)) } - } - portValue, err := strconv.ParseUint(port, 10, 16) - if err != nil { - logging.Error(ModuleName, "Server has invalid port value:", aurora.Cyan(port)) - continue - } + flagsBuffer = binary.BigEndian.AppendUint32(flagsBuffer, uint32(ip)) - flags |= NonstandardPortFlag - flagsBuffer = binary.BigEndian.AppendUint16(flagsBuffer, uint16(portValue)) + var port string + port, exists = server["publicport"] + if !exists { + // Fall back to local port if public port doesn't exist + if port, exists = server["localport"]; !exists { + logging.Error(ModuleName, "Server exists without port (publicip =", aurora.Cyan(publicip).String()+")") + continue + } + } - // Use the first local IP if it exists, this is used to skip natneg if multiple players are on the same network - if localip0, exists := server["localip0"]; exists { - flags |= PrivateIPFlag - - // localip is written like "192.168.255.255" for example, so it needs to be parsed - ipSplit := strings.Split(localip0, ".") - if len(ipSplit) != 4 { - logging.Error(ModuleName, "Server has invalid local IP:", aurora.Cyan(localip0)) + portValue, err := strconv.ParseUint(port, 10, 16) + if err != nil { + logging.Error(ModuleName, "Server has invalid port value:", aurora.Cyan(port)) continue } - err = nil - for _, s := range ipSplit { - val, err := strconv.ParseUint(s, 10, 8) - if err != nil { - break + if portValue < 1024 { + logging.Error(ModuleName, "Server uses reserved port:", aurora.Cyan(portValue)) + continue + } + + flags |= NonstandardPortFlag + flagsBuffer = binary.BigEndian.AppendUint16(flagsBuffer, uint16(portValue)) + + // Use the first local IP if it exists + if localip0, exists := server["localip0"]; exists { + flags |= PrivateIPFlag + + // localip is written like "192.168.255.255" for example, so it needs to be parsed + ipSplit := strings.Split(localip0, ".") + if len(ipSplit) != 4 { + logging.Error(ModuleName, "Server has invalid local IP:", aurora.Cyan(localip0)) + continue } - flagsBuffer = append(flagsBuffer, byte(val)) + err = nil + for _, s := range ipSplit { + val, err := strconv.ParseUint(s, 10, 8) + if err != nil { + break + } + + flagsBuffer = append(flagsBuffer, byte(val)) + } + + if err != nil { + logging.Error(ModuleName, "Server has invalid local IP value:", aurora.Cyan(localip0)) + continue + } } - if err != nil { - logging.Error(ModuleName, "Server has invalid local IP value:", aurora.Cyan(localip0)) - continue - } - } + if localport, exists := server["localport"]; exists { + portValue, err = strconv.ParseUint(localport, 10, 16) + if err != nil { + logging.Error(ModuleName, "Server has invalid local port value:", aurora.Cyan(localport)) + continue + } - if localport, exists := server["localport"]; exists { - portValue, err = strconv.ParseUint(localport, 10, 16) - if err != nil { - logging.Error(ModuleName, "Server has invalid local port value:", aurora.Cyan(localport)) + flags |= NonstandardPrivatePortFlag + flagsBuffer = binary.BigEndian.AppendUint16(flagsBuffer, uint16(portValue)) + } + + // Just a dummy IP? This is taken from dwc_network_server_emulator + // TODO: Check if this is actually needed + flags |= ICMPIPFlag + flagsBuffer = append(flagsBuffer, []byte{0, 0, 0, 0}...) + } else { + // Regular server, hide the public IP until match reservation is made + var searchIDStr string + if searchIDStr, exists = server["+searchid"]; !exists { + logging.Error(ModuleName, "Server exists without search ID") continue } - flags |= NonstandardPrivatePortFlag - flagsBuffer = binary.BigEndian.AppendUint16(flagsBuffer, uint16(portValue)) + searchID, err := strconv.ParseInt(searchIDStr, 10, 64) + if err != nil { + logging.Error(ModuleName, "Server has invalid search ID value:", aurora.Cyan(searchIDStr)) + } + + // Append low value as public IP + flagsBuffer = binary.BigEndian.AppendUint32(flagsBuffer, uint32(searchID&0xffffffff)) + // Append high value as public port + flags |= NonstandardPortFlag + flagsBuffer = binary.BigEndian.AppendUint16(flagsBuffer, uint16((searchID>>32)&0xffff)) } - // Just a dummy IP? This is taken from dwc_network_server_emulator - // TODO: Check if this is actually needed - flags |= ICMPIPFlag - flagsBuffer = append(flagsBuffer, []byte{0, 0, 0, 0}...) - - // Finally, write the server buffer to the output + // Append the server buffer to the output output = append(output, flags) output = append(output, flagsBuffer...) if (flags & HasKeysFlag) == 0 { // Server does not have keys, so skip them - logging.Info(ModuleName, "Wrote server without keys") continue } @@ -225,11 +265,9 @@ func handleServerListRequest(conn net.Conn, buffer []byte) { output = append(output, []byte(str)...) } - // Add null terminator so the string will be empty if the field doesn't exist + // Add null terminator output = append(output, 0x00) } - - logging.Info(ModuleName, "Wrote server with keys") } // Server with 0 flags and IP of 0xffffffff terminates the list @@ -240,10 +278,11 @@ func handleServerListRequest(conn net.Conn, buffer []byte) { } func handleSendMessageRequest(conn net.Conn, buffer []byte) { - // Read destination IP from buffer - destIP := fmt.Sprintf("%d.%d.%d.%d:%d", buffer[3], buffer[4], buffer[5], buffer[6], binary.BigEndian.Uint16(buffer[7:9])) + // Read search ID from buffer + searchID := uint64(binary.BigEndian.Uint32(buffer[3:7])) + searchID |= uint64(binary.BigEndian.Uint16(buffer[7:9])) << 32 - logging.Notice(ModuleName, "Send message from", aurora.BrightCyan(conn.RemoteAddr()), "to", aurora.BrightCyan(destIP).String()) + logging.Notice(ModuleName, "Send message from", aurora.BrightCyan(conn.RemoteAddr()), "to", aurora.Cyan(fmt.Sprintf("%012x", searchID))) - qr2.SendClientMessage(destIP, buffer[9:]) + qr2.SendClientMessage(conn.RemoteAddr().String(), searchID, buffer[9:]) }