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;
+ }
+
+ }
+}