mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-03-22 01:54:12 -05:00
ServerBrowser: Use random search ID rather than public IP
This commit is contained in:
parent
b9f2a7c9aa
commit
e510b2ccd6
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
10
qr2/main.go
10
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()
|
||||
|
|
|
|||
240
qr2/session.go
240
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 = "<UNKNOWN>"
|
||||
}
|
||||
|
||||
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 = "<UNKNOWN>"
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:])
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user