MASTER: Server list and session logic

This commit is contained in:
mkwcat
2023-10-24 02:37:57 -04:00
parent bdce41eaa4
commit b47fe639ae
5 changed files with 178 additions and 114 deletions

View File

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

View File

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

View File

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

View File

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

View File

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