diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config new file mode 100644 index 0000000..67f8ea0 --- /dev/null +++ b/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe new file mode 100644 index 0000000..c41a0d0 Binary files /dev/null and b/.nuget/NuGet.exe differ diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets new file mode 100644 index 0000000..3f8c37b --- /dev/null +++ b/.nuget/NuGet.targets @@ -0,0 +1,144 @@ + + + + $(MSBuildProjectDirectory)\..\ + + + false + + + false + + + true + + + false + + + + + + + + + + + $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) + + + + + $(SolutionDir).nuget + + + + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config + + + + $(MSBuildProjectDirectory)\packages.config + $(PackagesProjectConfig) + + + + + $(NuGetToolsPath)\NuGet.exe + @(PackageSource) + + "$(NuGetExePath)" + mono --runtime=v4.0.30319 "$(NuGetExePath)" + + $(TargetDir.Trim('\\')) + + -RequireConsent + -NonInteractive + + "$(SolutionDir) " + "$(SolutionDir)" + + + $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) + $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols + + + + RestorePackages; + $(BuildDependsOn); + + + + + $(BuildDependsOn); + BuildPackage; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PRMasterServer.sln b/PRMasterServer.sln index f5209dd..d0aafab 100644 --- a/PRMasterServer.sln +++ b/PRMasterServer.sln @@ -1,10 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30110.0 -MinimumVisualStudioVersion = 10.0.40219.1 +# Visual Studio Express 2012 for Windows Desktop Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRMasterServer", "PRMasterServer\PRMasterServer.csproj", "{64BC01A8-FCD2-4AB7-8811-3200F83AE124}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{13A3CA56-09D8-474B-9944-677FF9AEE98B}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/PRMasterServer/Data/NatNegMessage.cs b/PRMasterServer/Data/NatNegMessage.cs new file mode 100644 index 0000000..4b229bd --- /dev/null +++ b/PRMasterServer/Data/NatNegMessage.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Serialization; + +namespace PRMasterServer.Data +{ + public class NatNegMessage + { + public int Constant; // always 1e 66 6a b2 + public byte ProtocolVersion; + public byte RecordType; + public byte[] RecordSpecificData; + + public int ClientId; + public byte SequenceId; // (0x00 to 0x03) + public byte Hoststate; // (0x00 for guest, 0x01 for host) + public byte UseGamePort; + public string PrivateIPAddress; + public ushort LocalPort; + public string GameName; + + public string ClientPublicIPAddress; + public ushort ClientPublicPort; + public byte Error; + public byte GotData; + + public byte PortType; // (0x00, 0x80 or 0x90) + public byte ReplyFlag; + public ushort ConnectAckUnknown2; + public byte ConnectAckUnknown3; + public int ConnectAckUnknown4; + + public byte NatNegResult; + public int NatType; + public int NatMappingScheme; + + public byte ReportAckUnknown1; + public byte ReportAckUnknown2; + public ushort ReportAckUnknown3; + + public override string ToString() + { + if (RecordType == 0) return "INIT CLIENT " + ClientId + " SEQUENCE " + SequenceId + " HOSTSTATE " + Hoststate + " USEGAMEPORT " + UseGamePort + " PRIVATEIP " + PrivateIPAddress + " LOCALPORT " + LocalPort + " GAMENAME " + GameName; + if (RecordType == 1) return "INIT_ACK CLIENT " + ClientId + " SEQUENCE " + SequenceId + " HOSTSTATE " + Hoststate; + if (RecordType == 5) return "CONNECT CLIENT " + ClientId + " CLIENTPUBLICIP " + ClientPublicIPAddress + " CLIENTPUBLICPORT " + ClientPublicPort + " GOTDATA " + GotData + " ERROR " + Error; + if (RecordType == 6) return "CONNECT_ACK " + ClientId + " PORTTYPE " + PortType + " REPLYFLAG " + ReplyFlag + " UNKNOWN2 " + ConnectAckUnknown2 + " UNKNOWN3 " + ConnectAckUnknown3 + " UNKNOWN4 " + ConnectAckUnknown4; + if (RecordType == 13) return "REPORT " + ClientId + " PORTTYPE " + PortType + " HOSTSTATE " + Hoststate + " NATNEGRESULT " + NatNegResult + " NATTYPE " + NatType + " NATMAPPINGSCHEME " + NatMappingScheme + " GAMENAME " + GameName; + if (RecordType == 14) return "REPORT_ACK " + ClientId + " PORTTYPE " + PortType + " UNKNOWN1 " + ReportAckUnknown1 + " UNKNOWN2 " + ReportAckUnknown2 + " NATTYPE " + NatType + " UNKNOWN3 " + ReportAckUnknown3; + return "RECORDTYPE: " + RecordType; + } + + public static NatNegMessage ParseData(byte[] bytes) + { + if (bytes.Length < 8) return null; + if (bytes[0] != 0xFD || bytes[1] != 0xFC) return null; + NatNegMessage msg = new NatNegMessage(); + msg.Constant = _toInt(_getBytes(bytes, 2, 4)); + msg.ProtocolVersion = bytes[6]; + msg.RecordType = bytes[7]; + if (bytes.Length > 8) msg.RecordSpecificData = _getBytes(bytes, 8, bytes.Length - 8); + if (msg.RecordType == 0) + { + // INIT + msg.ClientId = _toInt(_getBytes(msg.RecordSpecificData, 0, 4)); + msg.SequenceId = msg.RecordSpecificData[4]; + msg.Hoststate = msg.RecordSpecificData[5]; + msg.UseGamePort = msg.RecordSpecificData[6]; + msg.PrivateIPAddress = _toIpAddress(_getBytes(msg.RecordSpecificData, 7, 4)); + msg.LocalPort = _toShort(_getBytes(msg.RecordSpecificData, 11, 2)); + msg.GameName = _toString(_getBytes(msg.RecordSpecificData, 13, msg.RecordSpecificData.Length-13)); + } + else if (msg.RecordType == 6) + { + // CONNECT_ACK + msg.ClientId = _toInt(_getBytes(msg.RecordSpecificData, 0, 4)); + msg.PortType = msg.RecordSpecificData[4]; + msg.ReplyFlag = msg.RecordSpecificData[5]; + msg.ConnectAckUnknown2 = _toShort(_getBytes(msg.RecordSpecificData, 6, 2)); + msg.ConnectAckUnknown3 = msg.RecordSpecificData[8]; + msg.ConnectAckUnknown4 = _toInt(_getBytes(msg.RecordSpecificData, 9, 4)); + } + else if (msg.RecordType == 13) + { + // CONNECT_ACK + msg.ClientId = _toInt(_getBytes(msg.RecordSpecificData, 0, 4)); + msg.PortType = msg.RecordSpecificData[4]; + msg.Hoststate = msg.RecordSpecificData[5]; + msg.NatNegResult = msg.RecordSpecificData[6]; + msg.NatType = _toIntBigEndian(_getBytes(msg.RecordSpecificData, 7, 4)); + msg.NatMappingScheme = _toIntBigEndian(_getBytes(msg.RecordSpecificData, 11, 4)); + msg.GameName = _toString(_getBytes(msg.RecordSpecificData, 15, msg.RecordSpecificData.Length - 15)); + } + return msg; + } + + public byte[] ToBytes() + { + List bytes = new List(); + bytes.Add(0xFD); + bytes.Add(0xFC); + bytes.Add(0x1E); + bytes.Add(0x66); + bytes.Add(0x6a); + bytes.Add(0xb2); + bytes.Add(ProtocolVersion); + bytes.Add(RecordType); + _addInt(bytes, ClientId); + if (RecordType == 1) + { + // INIT_ACK (0x01) + bytes.Add(SequenceId); + bytes.Add(Hoststate); + _addBytes(bytes, 0xFF, 0xFF); //ff ff unknown_5. Always 0xffff + _addBytes(bytes, 0x6d, 0x16, 0xb5, 0x7d, 0xea); // unknown_6. Any useless constant (also for other games). + } + else if (RecordType == 5) + { + // CONNECT (0x05) + _addIPAddress(bytes, ClientPublicIPAddress); + _addShort(bytes, ClientPublicPort); + bytes.Add(GotData); + bytes.Add(Error); + } + else if (RecordType == 14) + { + // REPORT_ACK + bytes.Add(PortType); + bytes.Add(ReportAckUnknown1); + bytes.Add(ReportAckUnknown2); + _addInt(bytes, NatType); + _addShort(bytes, ReportAckUnknown3); + } + return bytes.ToArray(); + } + + private static string _toString(byte[] bytes) { + List bs = new List(); + for (int i = 0; i < bytes.Length && bytes[i] > 0; i++) + bs.Add(bytes[i]); + return Encoding.ASCII.GetString(bs.ToArray()); + } + + private static void _addBytes(List bytes, params byte[] adds) + { + bytes.AddRange(adds); + } + + private static void _addInt(List bytes, int value) + { + List b = new List(BitConverter.GetBytes((int)value)); + if (BitConverter.IsLittleEndian) + { + b.Reverse(); + } + while (b.Count < 4) b.Insert(0, 0); + bytes.AddRange(b); + } + + private static void _addShort(List bytes, ushort value) + { + List b = new List(BitConverter.GetBytes(value)); + if (BitConverter.IsLittleEndian) + { + b.Reverse(); + } + while (b.Count < 2) b.Insert(0, 0); + bytes.AddRange(b); + } + + private static void _addIPAddress(List bytes, string address) + { + bytes.AddRange(address.Split('.').Select((b) => { return (byte)Convert.ToInt32(b); })); + } + + private static int _toInt(byte[] bytes) + { + return (int)bytes[0] * 256 * 256 * 256 + (int)bytes[1] * 256 * 256 + (int)bytes[2] * 256 + bytes[3]; + } + + private static int _toIntBigEndian(byte[] bytes) + { + return (int)bytes[3] * 256 * 256 * 256 + (int)bytes[2] * 256 * 256 + (int)bytes[1] * 256 + bytes[0]; + } + + private static ushort _toShort(byte[] bytes) + { + return (ushort)(bytes[0] * 256 + bytes[1]); + } + + public static string _toIpAddress(byte[] bytes) { + return string.Join(".", bytes.Select((b) => { return b.ToString(); })); + } + + private static byte[] _getBytes(byte[] bytes, int index, int nofBytes) { + byte[] result = new byte[nofBytes]; + for(int i = 0; i < nofBytes; i++) { + if(index+i >= bytes.Length) result[i] = 0; else result[i] = bytes[index+i]; + } + return result; + } + } +} diff --git a/PRMasterServer/Data/NatNegPeer.cs b/PRMasterServer/Data/NatNegPeer.cs new file mode 100644 index 0000000..9c5de07 --- /dev/null +++ b/PRMasterServer/Data/NatNegPeer.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; + +namespace PRMasterServer.Data +{ + public class NatNegPeer + { + public IPEndPoint PublicAddress; + public IPEndPoint CommunicationAddress; + public bool IsHost; + } + + public class NatNegClient + { + public int ClientId; + public NatNegPeer Host; + public NatNegPeer Guest; + } +} diff --git a/PRMasterServer/PRMasterServer.csproj b/PRMasterServer/PRMasterServer.csproj index 129447c..2cd2687 100644 --- a/PRMasterServer/PRMasterServer.csproj +++ b/PRMasterServer/PRMasterServer.csproj @@ -11,6 +11,8 @@ PRMasterServer v4.0 512 + ..\ + true true @@ -81,9 +83,13 @@ + + + + @@ -120,4 +126,5 @@ + \ No newline at end of file diff --git a/PRMasterServer/Program.cs b/PRMasterServer/Program.cs index 51d8360..74096f9 100644 --- a/PRMasterServer/Program.cs +++ b/PRMasterServer/Program.cs @@ -4,9 +4,19 @@ using System.Globalization; using System.Net; using System.Threading; +using System.Linq; +using System.Collections.Generic; namespace PRMasterServer { + /// + /// To run as a battlefield2 master server: + /// PRMasterServer.exe +db logindb + /// + /// To run as a (Civilization 4 Beyond the Sword) NAT Negotiation server: + /// PRMasterServer.exe +game civ4bts +servers master,natneg + /// + /// class Program { private static readonly object _lock = new object(); @@ -25,34 +35,94 @@ static void Main(string[] args) } }; - IPAddress bind = IPAddress.Any; - if (args.Length >= 1) { - for (int i = 0; i < args.Length; i++) { - if (args[i].Equals("+bind")) { - if ((i >= args.Length - 1) || !IPAddress.TryParse(args[i + 1], out bind)) { - LogError("+bind value must be a valid IP Address to bind to!"); - } - } else if (args[i].Equals("+db")) { - if ((i >= args.Length - 1)) { - LogError("+db value must be a path to the database"); - } else { - LoginDatabase.Initialize(args[i + 1], log, logError); - } - } - } - } + bool runLoginServer = true; + bool runNatNegServer = false; + bool runCdKeyServer = true; + bool runMasterServer = true; + bool runListServer = true; + string gameName = null; - if (!LoginDatabase.IsInitialized()) { - LogError("Error initializing database, please confirm parameter +db is valid"); - LogError("Press any key to continue"); - Console.ReadKey(); - return; - } + IPAddress bind = IPAddress.Any; + if (args.Length >= 1) + { + for (int i = 0; i < args.Length; i++) + { + if (args[i].Equals("+bind")) + { + if ((i >= args.Length - 1) || !IPAddress.TryParse(args[i + 1], out bind)) + { + LogError("+bind value must be a valid IP Address to bind to!"); + } + } + else if (args[i].Equals("+db")) + { + if ((i >= args.Length - 1)) + { + LogError("+db value must be a path to the database"); + } + else + { + LoginDatabase.Initialize(args[i + 1], log, logError); + } + } + else if (args[i].Equals("+game")) + { + if ((i >= args.Length - 1)) + { + LogError("+game value must be a game name"); + } + else + { + gameName = args[i + 1]; + } + } + else if (args[i].Equals("+servers")) + { + if ((i >= args.Length - 1)) + { + LogError("+servers value must be a comma-separated list of server types (master,login,cdkey,list,natneg)"); + } + else + { + List serverTypes = args[i + 1].Split(char.Parse(",")).Select(s => { return s.Trim().ToLower(); }).ToList(); + runLoginServer = serverTypes.IndexOf("login") >= 0; + runNatNegServer = serverTypes.IndexOf("natneg") >= 0; + runListServer = serverTypes.IndexOf("list") >= 0; + runMasterServer = serverTypes.IndexOf("master") >= 0; + runCdKeyServer = serverTypes.IndexOf("cdkey") >= 0; + } + } + } + } + + if (runLoginServer && !LoginDatabase.IsInitialized()) + { + LogError("Error initializing login database, please confirm parameter +db is valid"); + LogError("Press any key to continue"); + Console.ReadKey(); + return; + } - CDKeyServer cdKeyServer = new CDKeyServer(bind, 29910, log, logError); - ServerListReport serverListReport = new ServerListReport(bind, 27900, log, logError); - ServerListRetrieve serverListRetrieve = new ServerListRetrieve(bind, 28910, serverListReport, log, logError); - LoginServer loginServer = new LoginServer(bind, 29900, 29901, log, logError); + if (runCdKeyServer) + { + CDKeyServer serverCdKey = new CDKeyServer(bind, 29910, log, logError); + } + if (runMasterServer) + { + ServerListReport serverListReport = new ServerListReport(bind, 27900, log, logError, gameName); + if (runListServer) + { + ServerListRetrieve serverListRetrieve = new ServerListRetrieve(bind, 28910, serverListReport, log, logError); + } + } + if (runNatNegServer) + { + ServerNatNeg serverNatNeg = new ServerNatNeg(bind, 27901, log, logError); + } + if (runLoginServer) + { + LoginServer serverLogin = new LoginServer(bind, 29900, 29901, log, logError); + } while (true) { Thread.Sleep(1000); diff --git a/PRMasterServer/Servers/ServerListReport.cs b/PRMasterServer/Servers/ServerListReport.cs index 768d33a..216e899 100644 --- a/PRMasterServer/Servers/ServerListReport.cs +++ b/PRMasterServer/Servers/ServerListReport.cs @@ -36,10 +36,17 @@ internal class ServerListReport private byte[] _socketReceivedBuffer; // 09 then 4 00's then battlefield2 - private readonly byte[] _initialMessage = new byte[] { 0x09, 0x00, 0x00, 0x00, 0x00, 0x62, 0x61, 0x74, 0x74, 0x6c, 0x65, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x00 }; + private string _gameName = "battlefield2"; + private byte[] _initialMessage; - public ServerListReport(IPAddress listen, ushort port, Action log, Action logError) + public ServerListReport(IPAddress listen, ushort port, Action log, Action logError, string gameName) { + if (gameName != null) _gameName = gameName; + List initialMessage = new byte[] { 0x09, 0x00, 0x00, 0x00, 0x00 }.ToList(); + initialMessage.AddRange(Encoding.ASCII.GetBytes(_gameName)); + initialMessage.Add(0x00); + _initialMessage = initialMessage.ToArray(); + Log = log; LogError = logError; @@ -202,49 +209,61 @@ private void OnDataReceived(object sender, SocketAsyncEventArgs e) Array.Copy(e.Buffer, e.Offset, receivedBytes, 0, e.BytesTransferred); // there by a bunch of different message formats... - - if (receivedBytes.SequenceEqual(_initialMessage)) { - // the initial message is basically the gamename, 0x09 0x00 0x00 0x00 0x00 battlefield2 - // reply back a good response - byte[] response = new byte[] { 0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00 }; - _socket.SendTo(response, remote); - } else if (receivedBytes.Length > 5 && receivedBytes[0] == 0x03) { - // this is where server details come in, it starts with 0x03, it happens every 60 seconds or so - - byte[] uniqueId = new byte[4]; - Array.Copy(receivedBytes, 1, uniqueId, 0, 4); - - if (!ParseServerDetails(remote, receivedBytes.Skip(5).ToArray())) { - // this should be some sort of proper encrypted challenge, but for now i'm just going to hard code it because I don't know how the encryption works... - byte[] response = new byte[] { 0xfe, 0xfd, 0x01, uniqueId[0], uniqueId[1], uniqueId[2], uniqueId[3], 0x44, 0x3d, 0x73, 0x7e, 0x6a, 0x59, 0x30, 0x30, 0x37, 0x43, 0x39, 0x35, 0x41, 0x42, 0x42, 0x35, 0x37, 0x34, 0x43, 0x43, 0x00 }; - _socket.SendTo(response, remote); - } - } else if (receivedBytes.Length > 5 && receivedBytes[0] == 0x01) { - // this is a challenge response, it starts with 0x01 - - byte[] uniqueId = new byte[4]; - Array.Copy(receivedBytes, 1, uniqueId, 0, 4); - - // confirm against the hardcoded challenge - byte[] validate = new byte[] { 0x72, 0x62, 0x75, 0x67, 0x4a, 0x34, 0x34, 0x64, 0x34, 0x7a, 0x2b, 0x66, 0x61, 0x78, 0x30, 0x2f, 0x74, 0x74, 0x56, 0x56, 0x46, 0x64, 0x47, 0x62, 0x4d, 0x7a, 0x38, 0x41, 0x00 }; - byte[] clientResponse = new byte[validate.Length]; - Array.Copy(receivedBytes, 5, clientResponse, 0, clientResponse.Length); - - // if we validate, reply back a good response - if (clientResponse.SequenceEqual(validate)) { - byte[] response = new byte[] { 0xfe, 0xfd, 0x0a, uniqueId[0], uniqueId[1], uniqueId[2], uniqueId[3] }; - _socket.SendTo(response, remote); - - AddValidServer(remote); - } - } else if (receivedBytes.Length == 5 && receivedBytes[0] == 0x08) { - // this is a server ping, it starts with 0x08, it happens every 20 seconds or so - - byte[] uniqueId = new byte[4]; - Array.Copy(receivedBytes, 1, uniqueId, 0, 4); - RefreshServerPing(remote); - } + if (receivedBytes.SequenceEqual(_initialMessage)) + { + // the initial message is basically the gamename, 0x09 0x00 0x00 0x00 0x00 battlefield2 + // reply back a good response + byte[] response = new byte[] { 0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00 }; + _socket.SendTo(response, remote); + } + else + { + if (receivedBytes.Length > 5 && receivedBytes[0] == 0x03) + { + // this is where server details come in, it starts with 0x03, it happens every 60 seconds or so + + byte[] uniqueId = new byte[4]; + Array.Copy(receivedBytes, 1, uniqueId, 0, 4); + + if (!ParseServerDetails(remote, receivedBytes.Skip(5).ToArray())) + { + // this should be some sort of proper encrypted challenge, but for now i'm just going to hard code it because I don't know how the encryption works... + byte[] response = new byte[] { 0xfe, 0xfd, 0x01, uniqueId[0], uniqueId[1], uniqueId[2], uniqueId[3], 0x44, 0x3d, 0x73, 0x7e, 0x6a, 0x59, 0x30, 0x30, 0x37, 0x43, 0x39, 0x35, 0x41, 0x42, 0x42, 0x35, 0x37, 0x34, 0x43, 0x43, 0x00 }; + _socket.SendTo(response, remote); + } + } + else if (receivedBytes.Length > 5 && receivedBytes[0] == 0x01) + { + // this is a challenge response, it starts with 0x01 + + byte[] uniqueId = new byte[4]; + Array.Copy(receivedBytes, 1, uniqueId, 0, 4); + + // confirm against the hardcoded challenge + byte[] validate = new byte[] { 0x72, 0x62, 0x75, 0x67, 0x4a, 0x34, 0x34, 0x64, 0x34, 0x7a, 0x2b, 0x66, 0x61, 0x78, 0x30, 0x2f, 0x74, 0x74, 0x56, 0x56, 0x46, 0x64, 0x47, 0x62, 0x4d, 0x7a, 0x38, 0x41, 0x00 }; + byte[] clientResponse = new byte[validate.Length]; + Array.Copy(receivedBytes, 5, clientResponse, 0, clientResponse.Length); + + // if we validate, reply back a good response + if (clientResponse.SequenceEqual(validate)) + { + byte[] response = new byte[] { 0xfe, 0xfd, 0x0a, uniqueId[0], uniqueId[1], uniqueId[2], uniqueId[3] }; + _socket.SendTo(response, remote); + + AddValidServer(remote); + } + } + else if (receivedBytes.Length == 5 && receivedBytes[0] == 0x08) + { + // this is a server ping, it starts with 0x08, it happens every 20 seconds or so + + byte[] uniqueId = new byte[4]; + Array.Copy(receivedBytes, 1, uniqueId, 0, 4); + + RefreshServerPing(remote); + } + } } catch (Exception ex) { LogError(Category, ex.ToString()); } diff --git a/PRMasterServer/Servers/ServerNatNeg.cs b/PRMasterServer/Servers/ServerNatNeg.cs new file mode 100644 index 0000000..630d0cc --- /dev/null +++ b/PRMasterServer/Servers/ServerNatNeg.cs @@ -0,0 +1,249 @@ +using Alivate; +using MaxMind.GeoIP2; +using PRMasterServer.Data; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; + +namespace PRMasterServer.Servers +{ + internal class ServerNatNeg + { + private const string Category = "NatNegotiation"; + + public Action Log = (x, y) => { }; + public Action LogError = (x, y) => { }; + + public Thread Thread; + + private const int BufferSize = 65535; + private Socket _socket; + private SocketAsyncEventArgs _socketReadEvent; + private byte[] _socketReceivedBuffer; + private ConcurrentDictionary _Clients = new ConcurrentDictionary(); + + public ServerNatNeg(IPAddress listen, ushort port, Action log, Action logError) + { + Log = log; + LogError = logError; + + GeoIP.Initialize(log, Category); + + Thread = new Thread(StartServer) { + Name = "Server NatNeg Socket Thread" + }; + Thread.Start(new AddressInfo() { + Address = listen, + Port = port + }); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + try { + if (disposing) { + if (_socket != null) { + _socket.Close(); + _socket.Dispose(); + _socket = null; + } + } + } catch (Exception) { + } + } + + ~ServerNatNeg() + { + Dispose(false); + } + + private void StartServer(object parameter) + { + AddressInfo info = (AddressInfo)parameter; + Log(Category, "Starting Nat Neg Listener"); + try { + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) { + SendTimeout = 5000, + ReceiveTimeout = 5000, + SendBufferSize = BufferSize, + ReceiveBufferSize = BufferSize + }; + + _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); + _socket.Bind(new IPEndPoint(info.Address, info.Port)); + + _socketReadEvent = new SocketAsyncEventArgs() { + RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0) + }; + _socketReceivedBuffer = new byte[BufferSize]; + _socketReadEvent.SetBuffer(_socketReceivedBuffer, 0, BufferSize); + _socketReadEvent.Completed += OnDataReceived; + } catch (Exception e) { + LogError(Category, String.Format("Unable to bind Server List Reporting to {0}:{1}", info.Address, info.Port)); + LogError(Category, e.ToString()); + return; + } + + WaitForData(); + } + + private void WaitForData() + { + Thread.Sleep(10); + GC.Collect(); + + try { + _socket.ReceiveFromAsync(_socketReadEvent); + } catch (SocketException e) { + LogError(Category, "Error receiving data"); + LogError(Category, e.ToString()); + return; + } + } + + private void OnDataReceived(object sender, SocketAsyncEventArgs e) + { + /* + * Connection Protocol + * + * From http://wiki.tockdom.com/wiki/MKWii_Network_Protocol/Server/mariokartwii.natneg.gs.nintendowifi.net + * + * The NATNEG communication to enable a peer to peer communication is is done in the following steps: + * + * Both clients (called guest and host to distinguish them) exchange an unique natneg-id. In all observed Wii games this communication is done using Server MS and Server MASTER. + * Both clients sends independent of each other a sequence of 4 INIT packets to the NATNEG servers. The sequence number goes from 0 to 3. The guest sets the host_flag to 0 and the host to 1. The natneg-id must be the same for all packets. + * Packet 0 (sequence number 0) is send from the public address to server NATNEG1. This public address is later used for the peer to peer communication. + * Packet 1 (sequence number 1) is send from the communication address (usually an other port than the public address) to server NATNEG1. + * Packet 2 (sequence number 2) is send from the communication address to server NATNEG2 (any kind of fallback?). + * Packet 3 (sequence number 3) is send from the communication address to server NATNEG3 (any kind of fallback?). + * Each INIT packet is answered by an INIT_ACK packet as acknowledge to the original sender. + * If server NATNEG1 have received all 4 INIT packets with sequence numbers 0 and 1 (same natneg-id), then it sends 2 CONNECT packets: + * One packet is send to the communication address of the guest. The packet contains the public address of the host as data. + * The other packet is send to the communication address of the host. The packet contains the public address of the quest as data. + * Both clients send back a CONNECT_ACK packet to NATNEG1 as acknowledge. + * Both clients start peer to peer communication using the public addresses. + * + * C implementation: + * See http://aluigi.altervista.org/papers/gsnatneg.c + * + * Game names and game keys: + * Civilization IV: Beyond the Sword civ4bts Cs2iIq + * Mario Kart Wii (Wii) mariokartwii 9r3Rmy + * + */ + try { + IPEndPoint remote = (IPEndPoint)e.RemoteEndPoint; + + byte[] receivedBytes = new byte[e.BytesTransferred]; + Array.Copy(e.Buffer, e.Offset, receivedBytes, 0, e.BytesTransferred); + + NatNegMessage message = null; + try + { + message = NatNegMessage.ParseData(receivedBytes); + } + catch (Exception ex) + { + LogError(Category, ex.ToString()); + } + if (message == null) + { + Log(Category, "Received unknown data " + string.Join(" ", receivedBytes.Select((b) => { return b.ToString("X2"); }).ToArray()) + " from " + remote.ToString() ); + } + else + { + Log(Category, "Received message " + message.ToString() + " from " + remote.ToString()); + Log(Category, "(Message bytes: " + string.Join(" ", receivedBytes.Select((b) => { return b.ToString("X2"); }).ToArray()) + ")"); + if (message.RecordType == 0) + { + // INIT, return INIT_ACK + message.RecordType = 1; + SendResponse(remote, message); + + if (message.SequenceId > 1) + { + // Messages sent to natneg2 and natneg3, they only require an INIT_ACK. Used by client to determine NAT mapping mode? + } + else + { + // Collect data and send CONNECT messages if you have two peers initialized with all necessary data + if (!_Clients.ContainsKey(message.ClientId)) _Clients[message.ClientId] = new NatNegClient(); + NatNegClient client = _Clients[message.ClientId]; + client.ClientId = message.ClientId; + bool isHost = message.Hoststate > 0; + NatNegPeer peer = isHost ? client.Host : client.Guest; + if(peer == null) { + peer = new NatNegPeer(); + if(isHost) client.Host = peer; else client.Guest = peer; + } + peer.IsHost = isHost; + if(message.SequenceId == 0) + peer.PublicAddress = remote; + else + peer.CommunicationAddress = remote; + + if(client.Guest != null && client.Guest.CommunicationAddress != null && client.Guest.PublicAddress != null && client.Host != null && client.Host.CommunicationAddress != null && client.Host.PublicAddress != null) { + /* If server NATNEG1 have received all 4 INIT packets with sequence numbers 0 and 1 (same natneg-id), then it sends 2 CONNECT packets: + * One packet is send to the communication address of the guest. The packet contains the public address of the host as data. + * The other packet is send to the communication address of the host. The packet contains the public address of the quest as data. + */ + + // Remove client from dictionary + NatNegClient removed = null; + _Clients.TryRemove(client.ClientId, out removed); + + message.RecordType = 5; + message.Error = 0; + message.GotData = 0x42; + + message.ClientPublicIPAddress = NatNegMessage._toIpAddress(client.Host.PublicAddress.Address.GetAddressBytes()); + message.ClientPublicPort = (ushort)client.Host.PublicAddress.Port; + SendResponse(client.Guest.CommunicationAddress, message); + + message.ClientPublicIPAddress = NatNegMessage._toIpAddress(client.Guest.PublicAddress.Address.GetAddressBytes()); + message.ClientPublicPort = (ushort)client.Guest.PublicAddress.Port; + SendResponse(client.Host.CommunicationAddress, message); + + Log(Category, "Sent connect messages to peers with clientId " + client.ClientId + " connecting host " + client.Host.PublicAddress.ToString() + " and guest " + client.Guest.PublicAddress.ToString()); + } + } + } + else if (message.RecordType == 13) + { + // REPORT, return REPORT_ACK + message.RecordType = 14; + SendResponse(remote, message); + } + } + } catch (Exception ex) { + LogError(Category, ex.ToString()); + } + + WaitForData(); + } + + private void SendResponse(IPEndPoint remote, NatNegMessage message) + { + byte[] response = message.ToBytes(); + Log(Category, "Sending response " + message.ToString() + " to " + remote.ToString()); + Log(Category, "(Response bytes: " + string.Join(" ", response.Select((b) => { return b.ToString("X2"); }).ToArray()) + ")"); + _socket.SendTo(response, remote); + } + + } +} diff --git a/PRMasterServer/Utils/ChallengeEncryptor.cs b/PRMasterServer/Utils/ChallengeEncryptor.cs new file mode 100644 index 0000000..5d987ed --- /dev/null +++ b/PRMasterServer/Utils/ChallengeEncryptor.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace PRMasterServer.Utils +{ + /// + /// This code was started because I mistakenly thought that the NatNeg protocol used the Gamespy challenge encryption scheme. It doesn't, but this code remains, in case someone needs it for other uses. + /// The code was manually converted to C# from http://aluigi.altervista.org/papers/gsmsalg.h + /// + public class ChallengeEncryptor + { + private static byte[] enctype1_data { + get + { + if (_enctype1_data != null) return _enctype1_data; + _enctype1_data = enctype1_data_string.Split(' ').Select((b) => { return (byte)Convert.ToInt32(b, 16); }).ToArray(); + return _enctype1_data; + } + } + private static byte[] _enctype1_data = null; + private static string enctype1_data_string = + "01 ba fa b2 51 00 54 80 75 16 8e 8e 02 08 36 a5" + + " 2d 05 0d 16 52 07 b4 22 8c e9 09 d6 b9 26 00 04" + + " 06 05 00 13 18 c4 1e 5b 1d 76 74 fc 50 51 06 16" + + " 00 51 28 00 04 0a 29 78 51 00 01 11 52 16 06 4a" + + " 20 84 01 a2 1e 16 47 16 32 51 9a c4 03 2a 73 e1" + + " 2d 4f 18 4b 93 4c 0f 39 0a 00 04 c0 12 0c 9a 5e" + + " 02 b3 18 b8 07 0c cd 21 05 c0 a9 41 43 04 3c 52" + + " 75 ec 98 80 1d 08 02 1d 58 84 01 4e 3b 6a 53 7a" + + " 55 56 57 1e 7f ec b8 ad 00 70 1f 82 d8 fc 97 8b" + + " f0 83 fe 0e 76 03 be 39 29 77 30 e0 2b ff b7 9e" + + " 01 04 f8 01 0e e8 53 ff 94 0c b2 45 9e 0a c7 06" + + " 18 01 64 b0 03 98 01 eb 02 b0 01 b4 12 49 07 1f" + + " 5f 5e 5d a0 4f 5b a0 5a 59 58 cf 52 54 d0 b8 34" + + " 02 fc 0e 42 29 b8 da 00 ba b1 f0 12 fd 23 ae b6" + + " 45 a9 bb 06 b8 88 14 24 a9 00 14 cb 24 12 ae cc" + + " 57 56 ee fd 08 30 d9 fd 8b 3e 0a 84 46 fa 77 b8"; + + private static byte gsvalfunc(int reg) { + if(reg < 26) return (byte)(reg + 'A'); + if(reg < 52) return (byte)(reg + 'G'); + if(reg < 62) return (byte)(reg - 4); + if(reg == 62) return (byte)'+'; + if(reg == 63) return (byte)'/'; + return 0; + } + + private static byte[] gsseckey(byte[] src, byte[] key, int enctype) + { + int i, size, keysz; + byte[] enctmp = new byte[256]; + byte[] tmp = new byte[66]; + byte x, y, z, a, b; + + size = src.Length; + int len = ((size * 4) / 3) + 3; + byte[] dst = new byte[len]; + if ((size < 1) || (size > 65)) + { + dst[0] = 0; + return dst; + } + keysz = key.Length; + + for (i = 0; i < 256; i++) + { + enctmp[i] = (byte)i; + } + + a = 0; + for (i = 0; i < 256; i++) + { + a += (byte)(enctmp[i] + (byte)key[i % keysz]); + x = enctmp[a]; + enctmp[a] = enctmp[i]; + enctmp[i] = x; + } + + a = 0; + b = 0; + for (i = 0; src[i] > 0; i++) + { + a += (byte)(src[i] + 1); + x = enctmp[a]; + b += x; + y = enctmp[b]; + enctmp[b] = x; + enctmp[a] = y; + tmp[i] = (byte)(src[i] ^ enctmp[(x + y) & 0xff]); + } + for (size = i; size % 3 > 0; size++) + { + tmp[size] = 0; + } + + if (enctype == 1) + { + for (i = 0; i < size; i++) + { + tmp[i] = enctype1_data[tmp[i]]; + } + } + else if (enctype == 2) + { + for (i = 0; i < size; i++) + { + tmp[i] ^= key[i % keysz]; + } + } + + int p = 0; + for (i = 0; i < size; i += 3) + { + x = tmp[i]; + y = tmp[i + 1]; + z = tmp[i + 2]; + dst[p++] = gsvalfunc(x >> 2); + dst[p++] = gsvalfunc(((x & 3) << 4) | (y >> 4)); + dst[p++] = gsvalfunc(((y & 15) << 2) | (z >> 6)); + dst[p++] = gsvalfunc(z & 63); + } + return dst; + } + + } +}