From b47fe639ae35dd41faaf1bd47a829c780382f98d Mon Sep 17 00:00:00 2001 From: mkwcat Date: Tue, 24 Oct 2023 02:37:57 -0400 Subject: [PATCH] MASTER: Server list and session logic --- master/challenge.go | 43 ++++++++++---------- master/heartbeat.go | 51 +++++++++++++++++------- master/main.go | 92 ++++++++++++++++++++++++------------------- master/session.go | 79 +++++++++++++++++++++++++++++-------- matchmaking/server.go | 27 +++---------- 5 files changed, 178 insertions(+), 114 deletions(-) diff --git a/master/challenge.go b/master/challenge.go index 59b8579..eae91f9 100644 --- a/master/challenge.go +++ b/master/challenge.go @@ -8,34 +8,35 @@ import ( "wwfc/common" ) -func sendChallenge(conn net.PacketConn, addr net.Addr, sessionId uint32) { - addrString := strings.Split(addr.String(), ":") +func sendChallenge(conn net.PacketConn, addr net.Addr, session Session) { + challenge := session.Challenge + if challenge == "" { + // Generate challenge + addrString := strings.Split(addr.String(), ":") + var hexIP string + for _, i := range strings.Split(addrString[0], ".") { + val, err := strconv.ParseUint(i, 10, 64) + if err != nil { + panic(err) + } - // Generate challenge and send to server - var hexIP string - for _, i := range strings.Split(addrString[0], ".") { - val, err := strconv.ParseUint(i, 10, 64) + hexIP += fmt.Sprintf("%02X", val) + } + + port, err := strconv.ParseUint(addrString[1], 10, 64) if err != nil { panic(err) } - hexIP += fmt.Sprintf("%02X", val) + hexPort := fmt.Sprintf("%04X", port) + + challenge = common.RandomString(6) + "00" + hexIP + hexPort + mutex.Lock() + sessions[session.SessionID].Challenge = challenge + mutex.Unlock() } - port, err := strconv.ParseUint(addrString[1], 10, 64) - if err != nil { - panic(err) - } - - hexPort := fmt.Sprintf("%04X", port) - - challenge := common.RandomString(6) + "00" + hexIP + hexPort - mutex.Lock() - session := sessions[sessionId] - session.Challenge = challenge - mutex.Unlock() - - response := createResponseHeader(CommandChallenge, sessionId) + response := createResponseHeader(ChallengeRequest, session.SessionID) response = append(response, []byte(challenge)...) response = append(response, 0) diff --git a/master/heartbeat.go b/master/heartbeat.go index 1ede2ee..c43c342 100644 --- a/master/heartbeat.go +++ b/master/heartbeat.go @@ -11,7 +11,7 @@ import ( func heartbeat(conn net.PacketConn, addr net.Addr, buffer []byte) { sessionId := binary.BigEndian.Uint32(buffer[1:5]) - moduleName := "AVAILABLE:" + strconv.FormatInt(int64(sessionId), 10) + moduleName := "MASTER:" + strconv.FormatInt(int64(sessionId), 10) logging.Notice(moduleName, "Received heartbeat from", aurora.Cyan(addr).String()) values := strings.Split(string(buffer[5:]), "\u0000") @@ -26,28 +26,49 @@ func heartbeat(conn net.PacketConn, addr net.Addr, buffer []byte) { logging.Notice(moduleName, aurora.Cyan(values[i]).String()+":", aurora.Cyan(values[i+1]).String()) } - publicip, ok := payload["publicip"] - if !ok || publicip == "0" { - sendChallenge(conn, addr, sessionId) - return - } - - // TODO: Check if the client is registered - - statechanged, ok := payload["statechanged"] - if ok { + if statechanged, ok := payload["statechanged"]; ok { if statechanged == "1" { - // statechanged is 1 and publicip is not 0 // TODO: This would be a good place to run the server->client message exploit // for DNS patcher games that require code patches. The status code should be - // set to 5 at this point, which is required. - logging.Notice(moduleName, "Client server update") + // set to 5 at this point (if publicip is not 0), which is required. + logging.Notice(moduleName, "Client session update") // Fall through } if statechanged == "2" { - logging.Notice(moduleName, "Client server shutdown") + logging.Notice(moduleName, "Client session shutdown") + removeSession(sessionId) return } } + + 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] + + 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 := setSessionData(sessionId, payload) + if !session.Authenticated || !hasPublicIPKey || publicIPKey != realIP { + logging.Notice(moduleName, "Sending challenge") + sendChallenge(conn, addr, session) + return + } } diff --git a/master/main.go b/master/main.go index 3535049..9e8bd3e 100644 --- a/master/main.go +++ b/master/main.go @@ -4,7 +4,9 @@ import ( "encoding/binary" "github.com/logrusorgru/aurora/v3" "net" + "strconv" "sync" + "time" "wwfc/common" "wwfc/logging" ) @@ -16,17 +18,17 @@ var ( ) const ( - CommandQuery = 0x00 - CommandChallenge = 0x01 - CommandEcho = 0x02 - CommandHeartbeat = 0x03 - CommandAddError = 0x04 - CommandEchoResponse = 0x05 - CommandClientMessage = 0x06 - CommandClientMessageAck = 0x07 - CommandKeepAlive = 0x08 - CommandAvailable = 0x09 - CommandClientRegistered = 0x0A + QueryRequest = 0x00 + ChallengeRequest = 0x01 + EchoRequest = 0x02 + HeartbeatRequest = 0x03 + AddErrorRequest = 0x04 + EchoResponseRequest = 0x05 + ClientMessageRequest = 0x06 + ClientMessageAckRequest = 0x07 + KeepAliveRequest = 0x08 + AvailableRequest = 0x09 + ClientRegisteredReply = 0x0A ) func StartServer() { @@ -55,61 +57,71 @@ func StartServer() { } func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) { - if buffer[0] != 9 { - addSession(addr, buffer) + if buffer[0] == AvailableRequest { + logging.Notice("MASTER", "Command:", aurora.Yellow("AVAILABLE").String()) + conn.WriteTo(createResponseHeader(AvailableRequest, 0), addr) + return } + sessionId := binary.BigEndian.Uint32(buffer[1:5]) + moduleName := "MASTER:" + strconv.FormatInt(int64(sessionId), 10) + switch buffer[0] { - case CommandQuery: - logging.Notice("MASTER", "Command:", aurora.Yellow("QUERY").String()) + case QueryRequest: + logging.Notice(moduleName, "Command:", aurora.Yellow("QUERY").String()) break - case CommandChallenge: - logging.Notice("MASTER", "Command:", aurora.Yellow("CHALLENGE").String()) - sessionId := binary.BigEndian.Uint32(buffer[1:5]) - conn.WriteTo(createResponseHeader(CommandClientRegistered, sessionId), addr) + case ChallengeRequest: + logging.Notice(moduleName, "Command:", aurora.Yellow("CHALLENGE").String()) + + mutex.Lock() + sessions[sessionId].Authenticated = true + mutex.Unlock() + conn.WriteTo(createResponseHeader(ClientRegisteredReply, sessionId), addr) break - case CommandEcho: - logging.Notice("MASTER", "Command:", aurora.Yellow("ECHO").String()) + case EchoRequest: + logging.Notice(moduleName, "Command:", aurora.Yellow("ECHO").String()) break - case CommandHeartbeat: - logging.Notice("MASTER", "Command:", aurora.Yellow("HEARTBEAT").String()) + case HeartbeatRequest: + logging.Notice(moduleName, "Command:", aurora.Yellow("HEARTBEAT").String()) heartbeat(conn, addr, buffer) break - case CommandAddError: - logging.Notice("MASTER", "Command:", aurora.Yellow("ADDERROR").String()) + case AddErrorRequest: + logging.Notice(moduleName, "Command:", aurora.Yellow("ADDERROR").String()) break - case CommandEchoResponse: - logging.Notice("MASTER", "Command:", aurora.Yellow("ECHO_RESPONSE").String()) + case EchoResponseRequest: + logging.Notice(moduleName, "Command:", aurora.Yellow("ECHO_RESPONSE").String()) break - case CommandClientMessage: - logging.Notice("MASTER", "Command:", aurora.Yellow("CLIENT_MESSAGE").String()) + case ClientMessageRequest: + logging.Notice(moduleName, "Command:", aurora.Yellow("CLIENT_MESSAGE").String()) return - case CommandClientMessageAck: - logging.Notice("MASTER", "Command:", aurora.Yellow("CLIENT_MESSAGE_ACK").String()) + case ClientMessageAckRequest: + logging.Notice(moduleName, "Command:", aurora.Yellow("CLIENT_MESSAGE_ACK").String()) return - case CommandKeepAlive: - logging.Notice("MASTER", "Command:", aurora.Yellow("KEEPALIVE").String()) + case KeepAliveRequest: + logging.Notice(moduleName, "Command:", aurora.Yellow("KEEPALIVE").String()) + sessionId := binary.BigEndian.Uint32(buffer[1:5]) + mutex.Lock() + sessions[sessionId].LastKeepAlive = time.Now().Unix() + mutex.Unlock() return - case CommandAvailable: - logging.Notice("MASTER", "Command:", aurora.Yellow("AVAILABLE").String()) - conn.WriteTo(createResponseHeader(CommandAvailable, 0), addr) - break + case AvailableRequest: + return - case CommandClientRegistered: - logging.Notice("MASTER", "Command:", aurora.Yellow("QUERY").String()) + case ClientRegisteredReply: + logging.Notice(moduleName, "Command:", aurora.Yellow("CLIENT_REGISTERED").String()) break default: - logging.Notice("MASTER", "Unknown command:", aurora.Yellow(buffer[0]).String()) + logging.Notice(moduleName, "Unknown command:", aurora.Yellow(buffer[0]).String()) return } } diff --git a/master/session.go b/master/session.go index 9ed133e..206c515 100644 --- a/master/session.go +++ b/master/session.go @@ -1,29 +1,76 @@ package master import ( - "encoding/binary" - "net" + "github.com/logrusorgru/aurora/v3" + "time" + "wwfc/logging" +) + +const ( + ClientNoEndian = iota + ClientBigEndian + ClientLittleEndian ) type Session struct { - SessionID uint32 - Challenge string - SecretKey string - IsConnected bool + SessionID uint32 + Challenge string + Authenticated bool + LastKeepAlive int64 + Endianness byte // Some fields depend on the client's endianness + Data map[string]string } -func addSession(addr net.Addr, buffer []byte) { - sessionId := binary.BigEndian.Uint32(buffer[1:5]) +// 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 { mutex.Lock() - if _, ok := sessions[sessionId]; !ok { - sessions[sessionId] = &Session{ - SessionID: sessionId, - Challenge: "", - // TODO: This is hardcoded for Mario Kart Wii - SecretKey: "9r3Rmy", - IsConnected: true, + defer mutex.Unlock() + + session, exists := sessions[sessionId] + if !exists { + logging.Notice("MASTER", "Creating session", aurora.Cyan(sessionId).String()) + data := Session{ + SessionID: sessionId, + Challenge: "", + Authenticated: false, + LastKeepAlive: time.Now().Unix(), + Endianness: ClientNoEndian, + Data: payload, } + sessions[sessionId] = &data + return data } - mutex.Unlock() + + session.Data = payload + session.LastKeepAlive = time.Now().Unix() + return *session +} + +// Get a copy of the list of servers +func GetSessionServers() []map[string]string { + mutex.Lock() + defer mutex.Unlock() + + currentTime := time.Now().Unix() + + var servers []map[string]string + 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 + } + + servers = append(servers, session.Data) + } + + return servers } diff --git a/matchmaking/server.go b/matchmaking/server.go index d8ffee1..e93e562 100644 --- a/matchmaking/server.go +++ b/matchmaking/server.go @@ -8,6 +8,7 @@ import ( "strings" "wwfc/common" "wwfc/logging" + "wwfc/master" ) const ( @@ -27,36 +28,18 @@ const ( KeyTypeShort = 0x02 // Options for ServerListRequest + SendFieldsForAllOption = 1 << 0 // 0x01 / 1 NoServerListOption = 1 << 1 // 0x02 / 2 PushUpdatesOption = 1 << 2 // 0x04 / 4 AlternateSourceIPOption = 1 << 3 // 0x08 / 8 + SendGroupsOption = 1 << 5 // 0x20 / 32 NoListCacheOption = 1 << 6 // 0x40 / 64 LimitResultCountOption = 1 << 7 // 0x80 / 128 ) func FindServers(gueryGame string, filter string) ([]map[string]string, error) { - // TODO - - // This is a temporary hardcoded server - server := map[string]string{ - "localip0": "192.168.1.100", - "localport": "64174", - "natneg": "1", - "gamename": "mariokartwii", - "publicip": "2130706433", // 127.0.0.1 - "publicport": "64174", - "numplayers": "0", - "maxplayers": "11", - "dwc_pid": "27", - "dwc_mtype": "2", - "dwc_mver": "90", - "dwc_eval": "1", - "dwc_groupid": "100000027", - "dwc_hoststate": "2", - "dwc_suspend": "1", - } - - return []map[string]string{server}, nil + // TODO: Handle gueryGame, filter + return master.GetSessionServers(), nil } func popString(buffer []byte, index int) (string, int) {