From d145671262aa29e777396eceeb35924976e9b24e Mon Sep 17 00:00:00 2001 From: mkwcat Date: Mon, 6 Nov 2023 19:13:34 -0500 Subject: [PATCH] NATNEG: Fully implement the init command --- common/encoding.go | 38 -------- common/ip_address.go | 105 +++++++++++++++++++++ natneg/main.go | 211 ++++++++++++++++++++++++++++++++++++++++++- qr2/session.go | 16 +--- 4 files changed, 313 insertions(+), 57 deletions(-) create mode 100644 common/ip_address.go diff --git a/common/encoding.go b/common/encoding.go index 5cd0c0b..a342a16 100644 --- a/common/encoding.go +++ b/common/encoding.go @@ -2,8 +2,6 @@ package common import ( "fmt" - "strconv" - "strings" ) func Base32Encode(value int64) string { @@ -30,39 +28,3 @@ func reverse(s string) string { return string(rns) } - -func IPFormatToInt(ip string) (int32, uint16) { - port := 0 - - if strings.Contains(ip, ":") { - ipSplit := strings.Split(ip, ":") - - var err error - port, err = strconv.Atoi(ipSplit[1]) - if err != nil { - panic(err) - } - - ip = ipSplit[0] - } - - var intIP int - for i, s := range strings.Split(ip, ".") { - val, err := strconv.Atoi(s) - if err != nil { - panic(err) - } - - intIP |= val << (24 - i*8) - } - - // TODO: Check if this handles negative numbers properly - return int32(intIP), uint16(port) -} - -func IPFormatToString(ip string) (string, string) { - intIP, intPort := IPFormatToInt(ip) - - // TODO: Check if this handles negative numbers properly - return strconv.FormatInt(int64(intIP), 10), strconv.FormatUint(uint64(intPort), 10) -} diff --git a/common/ip_address.go b/common/ip_address.go new file mode 100644 index 0000000..e80c446 --- /dev/null +++ b/common/ip_address.go @@ -0,0 +1,105 @@ +package common + +import ( + "strconv" + "strings" +) + +func IPFormatToInt(ip string) (int32, uint16) { + port := 0 + + if strings.Contains(ip, ":") { + ipSplit := strings.Split(ip, ":") + + var err error + port, err = strconv.Atoi(ipSplit[1]) + if err != nil { + panic(err) + } + + ip = ipSplit[0] + } + + var intIP int + for i, s := range strings.Split(ip, ".") { + val, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + + intIP |= val << (24 - i*8) + } + + // TODO: Check if this handles negative numbers properly + return int32(intIP), uint16(port) +} + +func IPFormatNoPortToInt(ip string) int32 { + intIP, _ := IPFormatToInt(ip) + return intIP +} + +func IPFormatToString(ip string) (string, string) { + intIP, intPort := IPFormatToInt(ip) + + // TODO: Check if this handles negative numbers properly + return strconv.FormatInt(int64(intIP), 10), strconv.FormatUint(uint64(intPort), 10) +} + +func IPFormatBytes(ip string) []byte { + if strings.Contains(ip, ":") { + ip = strings.Split(ip, ":")[0] + } + + bytes := []byte{} + for _, s := range strings.Split(ip, ".") { + val, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + + bytes = append(bytes, byte(val)) + } + + return bytes +} + +var ( + reservedIPList = []struct { + ip int32 + mask int32 + }{ + {IPFormatNoPortToInt("0.0.0.0"), 8}, // RFC1122 "This host on this network" + {IPFormatNoPortToInt("10.0.0.0"), 8}, // RFC1918 Private-Use + {IPFormatNoPortToInt("100.64.0.0"), 10}, // RFC6598 Shared Address Space + {IPFormatNoPortToInt("127.0.0.0"), 8}, // RFC1122 Loopback + {IPFormatNoPortToInt("169.254.0.0"), 16}, // RFC3927 Link-Local + {IPFormatNoPortToInt("172.16.0.0"), 12}, // RFC1918 Private-Use + {IPFormatNoPortToInt("192.0.0.0"), 24}, // RFC6890 IETF Protocol Assignments + {IPFormatNoPortToInt("192.0.2.0"), 24}, // RFC5737 Documentation (TEST-NET-1) + {IPFormatNoPortToInt("192.31.196.0"), 24}, // RFC7535 AS112-v4 + {IPFormatNoPortToInt("192.52.193.0"), 24}, // RFC7450 AMT + {IPFormatNoPortToInt("192.88.99.0"), 24}, // RFC7526 6to4 Relay Anycast + {IPFormatNoPortToInt("192.168.0.0"), 16}, // RFC1918 Private-Use + {IPFormatNoPortToInt("192.175.48.0"), 24}, // RFC7534 Direct Delegation AS112 Service + {IPFormatNoPortToInt("198.18.0.0"), 15}, // RFC2544 Benchmarking + {IPFormatNoPortToInt("198.51.100.0"), 24}, // RFC5737 Documentation (TEST-NET-2) + {IPFormatNoPortToInt("203.0.113.0"), 24}, // RFC5737 Documentation (TEST-NET-3) + {IPFormatNoPortToInt("224.0.0.0"), 4}, // RFC1112 Multicast + {IPFormatNoPortToInt("240.0.0.0"), 4}, // RFC1112 Reserved for Future Use + RFC919 Limited Broadcast + } +) + +// TODO: Test this +func IsReservedIP(ip string) bool { + intIP, _ := IPFormatToInt(ip) + + for _, reserved := range reservedIPList { + rMask := 32 - reserved.mask + if intIP>>rMask == reserved.ip>>rMask { + return true + } + } + + return false +} diff --git a/natneg/main.go b/natneg/main.go index 5cbc8ec..01d6708 100644 --- a/natneg/main.go +++ b/natneg/main.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/logrusorgru/aurora/v3" "net" + "sync" "wwfc/common" "wwfc/logging" ) @@ -28,6 +29,48 @@ const ( NNReportReply = 0x0E NNPreInitRequest = 0x0F NNPreInitReply = 0x10 + + // Port type + PortTypeGamePort = 0x00 + PortTypeNATNEG1 = 0x01 + PortTypeNATNEG2 = 0x02 + PortTypeNATNEG3 = 0x03 + + // NAT type + NATTypeNoNat = 0x00 + NATTypeFirewallOnly = 0x01 + NATTypeFullCone = 0x02 + NATTypeRestrictedCone = 0x03 + NATTypePortRestrictedCone = 0x04 + NATTypeSymmetric = 0x05 + NATTypeUnknown = 0x06 + + // NAT mapping scheme + NATMappingUnknown = 0x00 + NATMappingSamePrivatePublic = 0x01 + NATMappingConsistent = 0x02 + NATMappingIncremental = 0x03 + NATMappingMixed = 0x04 +) + +type NATNEGSession struct { + Cookie uint32 + Mutex sync.RWMutex + Clients map[byte]*NATNEGClient +} + +type NATNEGClient struct { + Cookie uint32 + Connected bool + NegotiateIP string + LocalIP string + ServerIP string + GameName string +} + +var ( + sessions = map[uint32]*NATNEGSession{} + mutex = sync.RWMutex{} ) func StartServer() { @@ -45,13 +88,13 @@ func StartServer() { logging.Notice("NATNEG", "Listening on", address) for { - buf := make([]byte, 1024) - _, addr, err := conn.ReadFrom(buf) + buffer := make([]byte, 1024) + size, addr, err := conn.ReadFrom(buffer) if err != nil { continue } - go handleConnection(conn, addr, buf) + go handleConnection(conn, addr, buffer[:size]) } } @@ -68,18 +111,37 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) { // xx - Packet Type / Command // xx xx xx xx - Cookie - // version := buffer[6] + version := buffer[6] command := buffer[7] cookie := binary.BigEndian.Uint32(buffer[8:12]) - moduleName := "NATNEG:" + fmt.Sprintf("%08x", cookie) + addr.String() + ":" + moduleName := "NATNEG:" + fmt.Sprintf("%08x/", cookie) + addr.String() + + mutex.Lock() + session, exists := sessions[cookie] + if !exists { + // TODO: Figure out removing the session if this request is nonsense or something + logging.Notice(moduleName, "Creating session") + session = &NATNEGSession{ + Cookie: cookie, + Mutex: sync.RWMutex{}, + Clients: map[byte]*NATNEGClient{}, + } + sessions[cookie] = session + } + mutex.Unlock() + + session.Mutex.Lock() + defer session.Mutex.Unlock() switch command { default: logging.Error(moduleName, "Received unknown command type:", aurora.Cyan(command)) + break case NNInitRequest: logging.Notice(moduleName, "Command:", aurora.Yellow("NNInitRequest")) + session.handleInit(conn, addr, buffer[12:], moduleName, version) break case NNInitReply: @@ -104,6 +166,7 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) { case NNConnectReply: logging.Notice(moduleName, "Command:", aurora.Yellow("NNConnectReply")) + // TODO: Set the client Connected value to true here break case NNConnectPing: @@ -132,6 +195,7 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) { case NNReportRequest: logging.Notice(moduleName, "Command:", aurora.Yellow("NNReportRequest")) + session.handleReport(conn, addr, buffer[12:], moduleName, version) break case NNReportReply: @@ -147,3 +211,140 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) { break } } + +func getPortTypeName(portType byte) string { + switch portType { + default: + return fmt.Sprintf("Unknown (0x%02x)", portType) + + case PortTypeGamePort: + return "GamePort" + + case PortTypeNATNEG1: + return "NATNEG1" + + case PortTypeNATNEG2: + return "NATNEG2" + + case PortTypeNATNEG3: + return "NATNEG3" + } +} + +func (session *NATNEGSession) handleInit(conn net.PacketConn, addr net.Addr, buffer []byte, moduleName string, version byte) { + portType := buffer[0] + clientIndex := buffer[1] + useGamePort := buffer[2] + localIPBytes := buffer[3:7] + localPort := binary.BigEndian.Uint16(buffer[7:9]) + gameName := common.GetString(buffer[9:]) + + expectedSize := 9 + len(gameName) + 1 + if len(buffer) != expectedSize { + logging.Warn(moduleName, "Stray", aurora.BrightCyan(len(buffer)-expectedSize), "bytes after packet") + } + + localIPStr := fmt.Sprintf("%d.%d.%d.%d:%d", localIPBytes[0], localIPBytes[1], localIPBytes[2], localIPBytes[3], localPort) + + logging.Info(moduleName, "Game Name:", aurora.Cyan(gameName), "Version:", aurora.Cyan(version), "Port Type:", aurora.Yellow(getPortTypeName(portType)), "Client Index:", aurora.Cyan(clientIndex), "Use Game Port:", aurora.Cyan(useGamePort)) + logging.Info(moduleName, "Local IP:", aurora.Cyan(localIPStr)) + + if portType > 0x03 { + logging.Error(moduleName, "Invalid port type") + return + } + if useGamePort > 1 { + logging.Error(moduleName, "Invalid", aurora.BrightGreen("Use Game Port"), "value") + return + } + if useGamePort == 0 && portType == PortTypeGamePort { + logging.Error(moduleName, "Request uses game port but use game port is disabled") + return + } + + // Write the init acknowledgement to the requester address + ackHeader := createPacketHeader(version, NNInitReply, session.Cookie) + ackHeader = append(ackHeader, portType, clientIndex) + ackHeader = append(ackHeader, 0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea) + conn.WriteTo(ackHeader, addr) + + sender, exists := session.Clients[clientIndex] + if !exists { + logging.Notice(moduleName, "Creating client index", aurora.Cyan(clientIndex)) + sender = &NATNEGClient{ + Cookie: session.Cookie, + Connected: false, + NegotiateIP: "", + LocalIP: "", + ServerIP: "", + GameName: "", + } + session.Clients[clientIndex] = sender + } + + sender.Connected = false + sender.GameName = gameName + + if portType != PortTypeGamePort { + sender.NegotiateIP = addr.String() + } + if localPort != 0 { + sender.LocalIP = localIPStr + } + if useGamePort == 0 || portType == PortTypeGamePort { + sender.ServerIP = addr.String() + } + + if !sender.isMapped() { + return + } + logging.Notice(moduleName, "Mapped", aurora.BrightCyan(sender.NegotiateIP), aurora.BrightCyan(sender.LocalIP), aurora.BrightCyan(sender.ServerIP)) + + for id, destination := range session.Clients { + if id == clientIndex || destination.Connected || !destination.isMapped() { + continue + } + + logging.Notice(moduleName, "Exchange connect requests") + + // Send the requests back and forth + // TODO: Send again if no reply received from client + sender.sendConnectRequest(conn, destination, version) + destination.sendConnectRequest(conn, sender, version) + } +} + +func (client *NATNEGClient) isMapped() bool { + if client.NegotiateIP == "" || client.LocalIP == "" || client.ServerIP == "" { + return false + } + + return true +} + +func createPacketHeader(version byte, command byte, cookie uint32) []byte { + header := []byte{0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2, version, command} + return binary.BigEndian.AppendUint32(header, cookie) +} + +func (client *NATNEGClient) sendConnectRequest(conn net.PacketConn, destination *NATNEGClient, version byte) { + connectHeader := createPacketHeader(version, NNConnectRequest, destination.Cookie) + connectHeader = append(connectHeader, common.IPFormatBytes(client.ServerIP)...) + _, port := common.IPFormatToInt(client.ServerIP) + connectHeader = binary.BigEndian.AppendUint16(connectHeader, port) + // Two bytes: "gotyourdata" and "finished" + connectHeader = append(connectHeader, 0x42, 0x00) + + destIPAddr, err := net.ResolveUDPAddr("udp", destination.NegotiateIP) + if err != nil { + panic(err) + } + conn.WriteTo(connectHeader, destIPAddr) +} + +func (session *NATNEGSession) handleReport(conn net.PacketConn, addr net.Addr, buffer []byte, moduleName string, version byte) { + response := createPacketHeader(version, NNReportReply, session.Cookie) + response = append(response, buffer[:9]...) + response[14] = 0 + conn.WriteTo(response, addr) +} diff --git a/qr2/session.go b/qr2/session.go index fec89fe..ae544c5 100644 --- a/qr2/session.go +++ b/qr2/session.go @@ -5,7 +5,6 @@ import ( "github.com/logrusorgru/aurora/v3" "net" "strconv" - "strings" "time" "wwfc/common" "wwfc/gpcm" @@ -167,18 +166,7 @@ func GetSessionServers() []map[string]string { } func SendClientMessage(destIP string, message []byte) { - var rawIP int - for i, s := range strings.Split(strings.Split(destIP, ":")[0], ".") { - val, err := strconv.Atoi(s) - if err != nil { - panic(err) - } - - rawIP |= val << (24 - i*8) - } - - // TODO: Check if this handles negative numbers correctly - destIPIntStr := strconv.FormatInt(int64(int32(rawIP)), 10) + destIPIntStr, destPortStr := common.IPFormatToString(destIP) currentTime := time.Now().Unix() @@ -194,7 +182,7 @@ func SendClientMessage(destIP string, message []byte) { continue } - if session.Data["publicip"] == destIPIntStr { + if session.Data["publicip"] == destIPIntStr && session.Data["publicport"] == destPortStr { // Found the client, now send the message payload := createResponseHeader(ClientMessageRequest, session.SessionID) mutex.Unlock()