From 3f5850edd9bfddf3f6eb3e087c1266cf2163cbfb Mon Sep 17 00:00:00 2001 From: trippyone <137233897+trippyone@users.noreply.github.com> Date: Sat, 8 Jun 2024 10:40:39 -0400 Subject: [PATCH] Added STUN references + nat punch logic --- Fika.Core/Fika.Core.csproj | 3 + Fika.Core/Networking/FikaPingingClient.cs | 21 ++ .../Networking/Models/GetHostStunRequest.cs | 34 ++ .../Networking/Models/GetHostStunResponse.cs | 25 ++ .../Networking/NatPunch/FikaNatPunchClient.cs | 107 ++++++ .../Networking/NatPunch/FikaNatPunchServer.cs | 146 ++++++++ .../Networking/NatPunch/NatPunchUtils.cs | 76 ++++ .../Attributes/STUNOtherAddressAttribute.cs | 10 + .../Attributes/STUNResponseOriginAttribute.cs | 10 + .../STUNXorMappedAddressAttribute.cs | 10 + .../STUN/Attributes/StunAsciiTextAttribute.cs | 23 ++ .../Attributes/StunChangeRequestAttribute.cs | 51 +++ .../Attributes/StunChangedAddressAttribute.cs | 16 + .../STUN/Attributes/StunEndPointAttribute.cs | 61 +++ .../STUN/Attributes/StunErrorCodeAttribute.cs | 24 ++ .../Attributes/StunMappedAddressAttribute.cs | 16 + .../StunMessageIntegrityAttribute.cs | 21 ++ .../STUN/Attributes/StunPasswordAttribute.cs | 16 + .../Attributes/StunReflectedFromAttribute.cs | 21 ++ .../StunResponseAddressAttribute.cs | 16 + .../Attributes/StunSourceAddressAttribute.cs | 16 + .../STUN/Attributes/StunUsernameAttribute.cs | 16 + .../Networking/STUN/NATTypeDetectionRFC.cs | 8 + Fika.Core/Networking/STUN/STUNAttribute.cs | 84 +++++ .../Networking/STUN/STUNAttributeTypes.cs | 23 ++ Fika.Core/Networking/STUN/STUNBinaryReader.cs | 59 +++ Fika.Core/Networking/STUN/STUNBinaryWriter.cs | 57 +++ Fika.Core/Networking/STUN/STUNClient.cs | 89 +++++ Fika.Core/Networking/STUN/STUNErrorCode.cs | 21 ++ Fika.Core/Networking/STUN/STUNMessage.cs | 150 ++++++++ Fika.Core/Networking/STUN/STUNMessageTypes.cs | 18 + Fika.Core/Networking/STUN/STUNNATType.cs | 43 +++ .../STUN/STUNNatFilteringBehavior.cs | 9 + .../Networking/STUN/STUNNatMappingBehavior.cs | 9 + Fika.Core/Networking/STUN/STUNQueryError.cs | 35 ++ Fika.Core/Networking/STUN/STUNQueryResult.cs | 59 +++ Fika.Core/Networking/STUN/STUNQueryType.cs | 25 ++ Fika.Core/Networking/STUN/STUNRfc3489.cs | 353 ++++++++++++++++++ Fika.Core/Networking/STUN/STUNRfc5780.cs | 251 +++++++++++++ Fika.Core/Networking/STUN/STUNUtils.cs | 80 ++++ 40 files changed, 2112 insertions(+) create mode 100644 Fika.Core/Networking/Models/GetHostStunRequest.cs create mode 100644 Fika.Core/Networking/Models/GetHostStunResponse.cs create mode 100644 Fika.Core/Networking/NatPunch/FikaNatPunchClient.cs create mode 100644 Fika.Core/Networking/NatPunch/FikaNatPunchServer.cs create mode 100644 Fika.Core/Networking/NatPunch/NatPunchUtils.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/STUNOtherAddressAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/STUNResponseOriginAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/STUNXorMappedAddressAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunAsciiTextAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunChangeRequestAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunChangedAddressAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunEndPointAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunErrorCodeAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunMappedAddressAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunMessageIntegrityAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunPasswordAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunReflectedFromAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunResponseAddressAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunSourceAddressAttribute.cs create mode 100644 Fika.Core/Networking/STUN/Attributes/StunUsernameAttribute.cs create mode 100644 Fika.Core/Networking/STUN/NATTypeDetectionRFC.cs create mode 100644 Fika.Core/Networking/STUN/STUNAttribute.cs create mode 100644 Fika.Core/Networking/STUN/STUNAttributeTypes.cs create mode 100644 Fika.Core/Networking/STUN/STUNBinaryReader.cs create mode 100644 Fika.Core/Networking/STUN/STUNBinaryWriter.cs create mode 100644 Fika.Core/Networking/STUN/STUNClient.cs create mode 100644 Fika.Core/Networking/STUN/STUNErrorCode.cs create mode 100644 Fika.Core/Networking/STUN/STUNMessage.cs create mode 100644 Fika.Core/Networking/STUN/STUNMessageTypes.cs create mode 100644 Fika.Core/Networking/STUN/STUNNATType.cs create mode 100644 Fika.Core/Networking/STUN/STUNNatFilteringBehavior.cs create mode 100644 Fika.Core/Networking/STUN/STUNNatMappingBehavior.cs create mode 100644 Fika.Core/Networking/STUN/STUNQueryError.cs create mode 100644 Fika.Core/Networking/STUN/STUNQueryResult.cs create mode 100644 Fika.Core/Networking/STUN/STUNQueryType.cs create mode 100644 Fika.Core/Networking/STUN/STUNRfc3489.cs create mode 100644 Fika.Core/Networking/STUN/STUNRfc5780.cs create mode 100644 Fika.Core/Networking/STUN/STUNUtils.cs diff --git a/Fika.Core/Fika.Core.csproj b/Fika.Core/Fika.Core.csproj index 5982ec6a..9ef82a59 100644 --- a/Fika.Core/Fika.Core.csproj +++ b/Fika.Core/Fika.Core.csproj @@ -60,6 +60,9 @@ + + ..\References\websocket-sharp.dll + diff --git a/Fika.Core/Networking/FikaPingingClient.cs b/Fika.Core/Networking/FikaPingingClient.cs index 8dc5a14e..02219563 100644 --- a/Fika.Core/Networking/FikaPingingClient.cs +++ b/Fika.Core/Networking/FikaPingingClient.cs @@ -2,10 +2,12 @@ using Fika.Core.Coop.Matchmaker; using Fika.Core.Networking.Http; using Fika.Core.Networking.Http.Models; +using Fika.Core.Networking.NatPunch; using LiteNetLib; using LiteNetLib.Utils; using System.Net; using System.Net.Sockets; +using System.Threading.Tasks; namespace Fika.Core.Networking { @@ -16,6 +18,7 @@ internal class FikaPingingClient(string serverId) : INetEventListener private readonly string serverId = serverId; private IPEndPoint remoteEndPoint; private IPEndPoint localEndPoint; + private IPEndPoint remoteStunEndPoint; public bool Received = false; public bool Init() @@ -54,6 +57,19 @@ public bool Init() localEndPoint = new(IPAddress.Parse(localIp), port); } + //TODO: add config to enable this + + var localStunEndPoint = NatPunchUtils.CreateStunEndPoint(FikaPlugin.UDPPort.Value); + + FikaNatPunchClient fikaNatPunchClient = new FikaNatPunchClient(); + + fikaNatPunchClient.Connect(); + + GetHostStunRequest getStunRequest = new GetHostStunRequest(localStunEndPoint.Remote.Address.ToString(), localStunEndPoint.Remote.Port); + GetHostStunResponse getStunResponse = fikaNatPunchClient.GetHostStun(getStunRequest).Result; + + remoteStunEndPoint = new IPEndPoint(IPAddress.Parse(getStunResponse.StunIp), getStunResponse.StunPort); + NetClient.Start(); return true; @@ -74,6 +90,11 @@ public void PingEndPoint() { NetClient.SendUnconnectedMessage(writer, localEndPoint); } + + if (remoteStunEndPoint != null) + { + NetClient.SendUnconnectedMessage(writer, remoteStunEndPoint); + } } public void OnConnectionRequest(ConnectionRequest request) diff --git a/Fika.Core/Networking/Models/GetHostStunRequest.cs b/Fika.Core/Networking/Models/GetHostStunRequest.cs new file mode 100644 index 00000000..b1211cf6 --- /dev/null +++ b/Fika.Core/Networking/Models/GetHostStunRequest.cs @@ -0,0 +1,34 @@ +using Fika.Core.Coop.Components; +using SPT.Common.Http; +using System.Runtime.Serialization; + +namespace Fika.Core.Networking.Http.Models +{ + [DataContract] + public struct GetHostStunRequest + { + [DataMember(Name = "requestType")] + public string RequestType; + + [DataMember(Name = "clientId")] + public string ClientId; + + [DataMember(Name = "serverId")] + public string ServerId; + + [DataMember(Name = "stunIp")] + public string StunIp; + + [DataMember(Name = "stunPort")] + public int StunPort; + + public GetHostStunRequest(string stunIp, int stunPort) + { + RequestType = GetType().Name; + ClientId = RequestHandler.SessionId; + ServerId = CoopHandler.GetServerId(); + StunIp = stunIp; + StunPort = stunPort; + } + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/Models/GetHostStunResponse.cs b/Fika.Core/Networking/Models/GetHostStunResponse.cs new file mode 100644 index 00000000..7c992ab7 --- /dev/null +++ b/Fika.Core/Networking/Models/GetHostStunResponse.cs @@ -0,0 +1,25 @@ +using System.Runtime.Serialization; + +[DataContract] +public struct GetHostStunResponse +{ + [DataMember(Name = "requestType")] + public string RequestType; + + [DataMember(Name = "clientId")] + public string ClientId; + + [DataMember(Name = "StunIp")] + public string StunIp; + + [DataMember(Name = "StunPort")] + public int StunPort; + + public GetHostStunResponse(string clientId, string stunIp, int stunPort) + { + RequestType = GetType().Name; + ClientId = clientId; + StunIp = stunIp; + StunPort = stunPort; + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/NatPunch/FikaNatPunchClient.cs b/Fika.Core/Networking/NatPunch/FikaNatPunchClient.cs new file mode 100644 index 00000000..9cf88136 --- /dev/null +++ b/Fika.Core/Networking/NatPunch/FikaNatPunchClient.cs @@ -0,0 +1,107 @@ +using WebSocketSharp; +using System; +using Newtonsoft.Json; +using System.Threading.Tasks; +using Fika.Core.Networking.Http.Models; +using SPT.Common.Http; + +namespace Fika.Core.Networking.NatPunch +{ + public class FikaNatPunchClient + { + public string Host { get; set; } + public string Url { get; set; } + public string SessionId { get; set; } + + private WebSocket _webSocket; + private TaskCompletionSource _receiveTaskCompletion; + + public StunIpEndPoint StunIpEndPoint { get; set; } + + public FikaNatPunchClient() + { + // Assuming http protocol is always used + Host = RequestHandler.Host.Replace("http", "ws"); + SessionId = RequestHandler.SessionId; + Url = $"{Host}/{SessionId}?"; + + _webSocket = new WebSocket(Url) + { + WaitTime = TimeSpan.FromMinutes(1), + EmitOnPing = true + }; + + _webSocket.OnOpen += WebSocket_OnOpen; + _webSocket.OnError += WebSocket_OnError; + _webSocket.OnMessage += WebSocket_OnMessage; + } + + public void Connect() + { + _webSocket.Connect(); + } + + public void Close() + { + _webSocket.Close(); + } + + private void WebSocket_OnOpen(object sender, EventArgs e) + { + EFT.UI.ConsoleScreen.Log("Connected to FikaNatPunchService as client"); + } + + private void WebSocket_OnMessage(object sender, MessageEventArgs e) + { + if (_receiveTaskCompletion == null) + return; + + if (_receiveTaskCompletion.Task.Status == TaskStatus.RanToCompletion) + return; + + if (e == null) + return; + + if (string.IsNullOrEmpty(e.Data)) + return; + + _receiveTaskCompletion.SetResult(e.Data); + } + + private void WebSocket_OnError(object sender, ErrorEventArgs e) + { + EFT.UI.ConsoleScreen.LogError($"Websocket error {e}"); + _webSocket.Close(); + } + + private void Send(T1 o) + { + var data = JsonConvert.SerializeObject(o); + _webSocket.Send(data); + } + + private async Task Receive() + { + var data = await _receiveTaskCompletion.Task; + var obj = JsonConvert.DeserializeObject(data); + + return obj; + } + + private async Task SendAndReceiveAsync(T1 o) + { + _receiveTaskCompletion = new TaskCompletionSource(); + + Send(o); + + var data = await Receive(); + + return data; + } + + public async Task GetHostStun(GetHostStunRequest getHostStunRequest) + { + return await SendAndReceiveAsync(getHostStunRequest); + } + } +} diff --git a/Fika.Core/Networking/NatPunch/FikaNatPunchServer.cs b/Fika.Core/Networking/NatPunch/FikaNatPunchServer.cs new file mode 100644 index 00000000..59232bff --- /dev/null +++ b/Fika.Core/Networking/NatPunch/FikaNatPunchServer.cs @@ -0,0 +1,146 @@ +using LiteNetLib; +using System; +using System.Net; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; +using SPT.Common.Http; +using WebSocketSharp; +using Fika.Core.Networking.Http.Models; + +namespace Fika.Core.Networking.NatPunch +{ + public class FikaNatPunchServer + { + public string Host { get; set; } + public string Url { get; set; } + public string SessionId { get; set; } + public StunIpEndPoint StunIpEndPoint { get; set; } + + private WebSocket _webSocket; + private NetManager _netManager; + + public FikaNatPunchServer(NetManager netManager) + { + // Assuming http protocol is always used + Host = RequestHandler.Host.Replace("http", "ws"); + SessionId = RequestHandler.SessionId; + Url = $"{Host}/{SessionId}?"; + + _webSocket = new WebSocket(Url) + { + WaitTime = TimeSpan.FromMinutes(1), + EmitOnPing = true + }; + + _webSocket.OnOpen += WebSocket_OnOpen; + _webSocket.OnError += WebSocket_OnError; + _webSocket.OnMessage += WebSocket_OnMessage; + + _netManager = netManager; + + StunIpEndPoint = NatPunchUtils.CreateStunEndPoint(FikaPlugin.UDPPort.Value); + } + + public void Listen() + { + _webSocket.Connect(); + } + + public void Close() + { + _webSocket.Close(); + } + + private void WebSocket_OnOpen(object sender, EventArgs e) + { + EFT.UI.ConsoleScreen.Log("Connected to FikaNatPunchService as server"); + } + + private void WebSocket_OnMessage(object sender, MessageEventArgs e) + { + if (e == null) + return; + + if (string.IsNullOrEmpty(e.Data)) + return; + + ProcessMessage(e.Data); + } + + private void WebSocket_OnError(object sender, ErrorEventArgs e) + { + EFT.UI.ConsoleScreen.LogError($"Websocket error {e}"); + _webSocket.Close(); + } + + private void ProcessMessage(string data) + { + EFT.UI.ConsoleScreen.Log($"data: {data}"); + var msgObj = GetRequestObject(data); + + var msgObjType = msgObj.GetType().Name; + + EFT.UI.ConsoleScreen.Log($"msgObj: {msgObjType}"); + + switch (msgObjType) + { + case "GetHostStunRequest": + var getHostStunRequest = (GetHostStunRequest)msgObj; + EFT.UI.ConsoleScreen.Log($"received request GetHostStunRequest: {getHostStunRequest.StunIp}:{getHostStunRequest.StunPort}"); + + if (StunIpEndPoint != null) + { + IPEndPoint clientIpEndPoint = new IPEndPoint(IPAddress.Parse(getHostStunRequest.StunIp), getHostStunRequest.StunPort); + + EFT.UI.ConsoleScreen.Log($"parsed GetHostStunRequest: {clientIpEndPoint.Address.ToString()}:{clientIpEndPoint.Port}"); + + NatPunchUtils.PunchNat(_netManager, clientIpEndPoint); + + EFT.UI.ConsoleScreen.Log($"PUNCHED"); + + EFT.UI.ConsoleScreen.Log($"Sending GetHostStunResponse...:"); + SendHostStun(getHostStunRequest.ClientId, StunIpEndPoint); + EFT.UI.ConsoleScreen.Log($"Sent GetHostStunResponse...:"); + } + + break; + } + } + + private void Send(T1 o) + { + var data = JsonConvert.SerializeObject(o); + _webSocket.Send(data); + } + + private object GetRequestObject(string data) + { + // We're doing this literally once, so this is fine. Might need to + // refactor to use StreamReader to detect request type later. + JObject obj = JObject.Parse(data); + + EFT.UI.ConsoleScreen.Log(data.ToString()); + + if (!obj.ContainsKey("requestType")) + { + throw new NullReferenceException("requestType"); + } + + var requestType = obj["requestType"].ToString(); + + switch (requestType) + { + case "GetHostStunRequest": + return JsonConvert.DeserializeObject(data); + default: + throw new ArgumentException("requestType"); + } + } + + public void SendHostStun(string clientId, StunIpEndPoint stunIpEndPoint) + { + var getHostStunResponse = new GetHostStunResponse(clientId, stunIpEndPoint.Remote.Address.ToString(), stunIpEndPoint.Remote.Port); + Send(getHostStunResponse); + } + } +} diff --git a/Fika.Core/Networking/NatPunch/NatPunchUtils.cs b/Fika.Core/Networking/NatPunch/NatPunchUtils.cs new file mode 100644 index 00000000..8ec53813 --- /dev/null +++ b/Fika.Core/Networking/NatPunch/NatPunchUtils.cs @@ -0,0 +1,76 @@ +using LiteNetLib.Utils; +using LiteNetLib; +using STUN; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Fika.Core.Networking.NatPunch +{ + public class StunIpEndPoint + { + public IPEndPoint Local { get; set; } + public IPEndPoint Remote { get; set; } + + public StunIpEndPoint(IPEndPoint localIpEndPoint, IPEndPoint remoteIpEndPoint) + { + Local = localIpEndPoint; + Remote = remoteIpEndPoint; + } + } + + public static class NatPunchUtils + { + public static StunIpEndPoint CreateStunEndPoint(int localPort = 0) + { + var stunUdpClient = new UdpClient(); + var stunQueryResult = new STUNQueryResult(); + + try + { + if (localPort > 0) + stunUdpClient.Client.Bind(new IPEndPoint(IPAddress.Any, localPort)); + + IPAddress stunIp = Array.Find(Dns.GetHostEntry("stun.l.google.com").AddressList, a => a.AddressFamily == AddressFamily.InterNetwork); + int stunPort = 19302; + + var stunQueryIpEndPoint = new IPEndPoint(stunIp, stunPort); + + stunQueryResult = STUNClient.Query(stunUdpClient.Client, stunQueryIpEndPoint, STUNQueryType.ExactNAT, NATTypeDetectionRFC.Rfc3489); + + if (stunQueryResult.PublicEndPoint != null) + { + var stunIpEndPointResult = new StunIpEndPoint((IPEndPoint)stunUdpClient.Client.LocalEndPoint, stunQueryResult.PublicEndPoint); + return stunIpEndPointResult; + } + } + catch (Exception ex) + { + //log exception + } + finally + { + stunUdpClient.Client.Close(); + } + + return null; + } + + public static void PunchNat(NetManager netManager, IPEndPoint endPoint) + { + // bogus punch data + var resp = new NetDataWriter(); + resp.Put(9999); + + // send a couple of packets to punch a hole + for (int i = 0; i < 10; i++) + { + netManager.SendUnconnectedMessage(resp, endPoint); + } + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/STUNOtherAddressAttribute.cs b/Fika.Core/Networking/STUN/Attributes/STUNOtherAddressAttribute.cs new file mode 100644 index 00000000..0b2b48ed --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/STUNOtherAddressAttribute.cs @@ -0,0 +1,10 @@ +namespace STUN.Attributes +{ + public class STUNOtherAddressAttribute : STUNEndPointAttribute + { + public override string ToString() + { + return string.Format("OTHER-ADDRESS {0}", EndPoint); + } + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/STUN/Attributes/STUNResponseOriginAttribute.cs b/Fika.Core/Networking/STUN/Attributes/STUNResponseOriginAttribute.cs new file mode 100644 index 00000000..35975a9b --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/STUNResponseOriginAttribute.cs @@ -0,0 +1,10 @@ +namespace STUN.Attributes +{ + public class STUNResponseOriginAttribute : STUNEndPointAttribute + { + public override string ToString() + { + return string.Format("RESPONSE-ORIGIN {0}", EndPoint); + } + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/STUN/Attributes/STUNXorMappedAddressAttribute.cs b/Fika.Core/Networking/STUN/Attributes/STUNXorMappedAddressAttribute.cs new file mode 100644 index 00000000..b947ae63 --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/STUNXorMappedAddressAttribute.cs @@ -0,0 +1,10 @@ +namespace STUN.Attributes +{ + public class STUNXorMappedAddressAttribute : STUNEndPointAttribute + { + public override string ToString() + { + return string.Format("XOR-MAPPED-ADDRESS {0}", EndPoint); + } + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/STUN/Attributes/StunAsciiTextAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunAsciiTextAttribute.cs new file mode 100644 index 00000000..596abf25 --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunAsciiTextAttribute.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNAsciiTextAttribute : STUNAttribute + { + public string Text { get; set; } + + public override void Parse(STUNBinaryReader binary, int length) + { + Text = Encoding.ASCII.GetString(binary.ReadBytes(length)); + } + + public override void WriteBody(STUNBinaryWriter binary) + { + binary.Write(Encoding.ASCII.GetBytes(Text)); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunChangeRequestAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunChangeRequestAttribute.cs new file mode 100644 index 00000000..5bfba175 --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunChangeRequestAttribute.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNChangeRequestAttribute : STUNAttribute + { + private static readonly byte[] Three0 = new byte[3]; + + public bool ChangeIP { get; set; } + public bool ChangePort { get; set; } + + public STUNChangeRequestAttribute() + { + + } + + public STUNChangeRequestAttribute(bool ip, bool port) + { + ChangeIP = ip; + ChangePort = port; + } + + public override void Parse(STUNBinaryReader binary, int length) + { + binary.BaseStream.Position += 3; + var b = binary.ReadByte(); + ChangeIP = ((b & 4) != 0); + ChangePort = ((b & 2) != 0); + } + + public override void WriteBody(STUNBinaryWriter binary) + { + binary.Write(Three0); + + int i = 0; + if (ChangeIP) i |= 4; + if (ChangePort) i |= 2; + + binary.Write((byte)i); + } + + public override string ToString() + { + return string.Format("CHANGE-REQUEST IP:{0} PORT:{1}", ChangeIP, ChangePort); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunChangedAddressAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunChangedAddressAttribute.cs new file mode 100644 index 00000000..d794f7d8 --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunChangedAddressAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNChangedAddressAttribute : STUNEndPointAttribute + { + public override string ToString() + { + return string.Format("CHANGE(D)-ADDRESS {0}", EndPoint); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunEndPointAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunEndPointAttribute.cs new file mode 100644 index 00000000..fab21cae --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunEndPointAttribute.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Net; +using System.Net.Sockets; + +namespace STUN.Attributes +{ + public class STUNEndPointAttribute : STUNAttribute + { + public IPEndPoint EndPoint { get; set; } + + public override void Parse(STUNBinaryReader binary, int length) + { + binary.BaseStream.Position++; + var ipFamily = binary.ReadByte(); + var port = binary.ReadUInt16(); + IPAddress address; + + if (ipFamily == 1) + { + address = new IPAddress(binary.ReadBytes(4)); + } + else if (ipFamily == 2) + { + address = new IPAddress(binary.ReadBytes(16)); + } + else + { + throw new Exception("Unsupported IP Family " + ipFamily.ToString()); + } + + EndPoint = new IPEndPoint(address, port); + } + + public override void WriteBody(STUNBinaryWriter binary) + { + binary.Write((byte)0); + + if (EndPoint.AddressFamily == AddressFamily.InterNetwork) + { + binary.Write((byte)1); + } + else if (EndPoint.AddressFamily == AddressFamily.InterNetworkV6) + { + binary.Write((byte)2); + } + else + { + throw new Exception("Unsupported IP Family" + EndPoint.AddressFamily.ToString()); + } + + binary.Write((ushort)EndPoint.Port); + + var addressBytes = EndPoint.Address.GetAddressBytes(); + binary.Write(addressBytes); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunErrorCodeAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunErrorCodeAttribute.cs new file mode 100644 index 00000000..c272c280 --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunErrorCodeAttribute.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNErrorCodeAttribute : STUNAttribute + { + public STUNErrorCodes Error { get; set; } + public string Phrase { get; set; } + + public override void Parse(STUNBinaryReader binary, int length) + { + throw new NotImplementedException(); + } + + public override void WriteBody(STUNBinaryWriter binary) + { + throw new NotImplementedException(); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunMappedAddressAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunMappedAddressAttribute.cs new file mode 100644 index 00000000..0379275c --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunMappedAddressAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNMappedAddressAttribute : STUNEndPointAttribute + { + public override string ToString() + { + return string.Format("MAPPED-ADDRESS {0}", EndPoint); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunMessageIntegrityAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunMessageIntegrityAttribute.cs new file mode 100644 index 00000000..52b27e36 --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunMessageIntegrityAttribute.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNMessageIntegrityAttribute : STUNAttribute + { + public override void Parse(STUNBinaryReader binary, int length) + { + throw new NotImplementedException(); + } + + public override void WriteBody(STUNBinaryWriter binary) + { + throw new NotImplementedException(); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunPasswordAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunPasswordAttribute.cs new file mode 100644 index 00000000..8a51fa79 --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunPasswordAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNPasswordAttribute : STUNAsciiTextAttribute + { + public override string ToString() + { + return string.Format("PASSWORD \"{0}\"", Text); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunReflectedFromAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunReflectedFromAttribute.cs new file mode 100644 index 00000000..576cf7f4 --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunReflectedFromAttribute.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNReflectedFromAttribute : STUNAttribute + { + public override void Parse(STUNBinaryReader binary, int length) + { + throw new NotImplementedException(); + } + + public override void WriteBody(STUNBinaryWriter binary) + { + throw new NotImplementedException(); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunResponseAddressAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunResponseAddressAttribute.cs new file mode 100644 index 00000000..d291fb64 --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunResponseAddressAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNResponseAddressAttribute : STUNEndPointAttribute + { + public override string ToString() + { + return string.Format("RESPONSE-ADDRESS {0}", EndPoint); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunSourceAddressAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunSourceAddressAttribute.cs new file mode 100644 index 00000000..46b0ccfb --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunSourceAddressAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNSourceAddressAttribute : STUNEndPointAttribute + { + public override string ToString() + { + return string.Format("SOURCE-ADDRESS {0}", EndPoint); + } + } +} diff --git a/Fika.Core/Networking/STUN/Attributes/StunUsernameAttribute.cs b/Fika.Core/Networking/STUN/Attributes/StunUsernameAttribute.cs new file mode 100644 index 00000000..e588f481 --- /dev/null +++ b/Fika.Core/Networking/STUN/Attributes/StunUsernameAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN.Attributes +{ + public class STUNUsernameAttribute : STUNAsciiTextAttribute + { + public override string ToString() + { + return string.Format("USERNAME \"{0}\"", Text); + } + } +} diff --git a/Fika.Core/Networking/STUN/NATTypeDetectionRFC.cs b/Fika.Core/Networking/STUN/NATTypeDetectionRFC.cs new file mode 100644 index 00000000..e09be666 --- /dev/null +++ b/Fika.Core/Networking/STUN/NATTypeDetectionRFC.cs @@ -0,0 +1,8 @@ +namespace STUN +{ + public enum NATTypeDetectionRFC + { + Rfc3489, + Rfc5780 + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/STUN/STUNAttribute.cs b/Fika.Core/Networking/STUN/STUNAttribute.cs new file mode 100644 index 00000000..1b018a2a --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNAttribute.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using STUN.Attributes; + +namespace STUN +{ + public abstract class STUNAttribute + { + static Dictionary _knownAttributes; + static Dictionary _knownTypes; + + static STUNAttribute() + { + _knownAttributes = new Dictionary(); + _knownTypes = new Dictionary(); + + AddAttribute(1); + AddAttribute(2); + AddAttribute(3); + AddAttribute(4); + AddAttribute(5); + AddAttribute(6); + AddAttribute(7); + AddAttribute(8); + AddAttribute(9); + //AddAttribute<>(10); + AddAttribute(11); + AddAttribute(0x0020); + AddAttribute(0x802B); + AddAttribute(0x802C); + } + + public abstract void Parse(STUNBinaryReader binary, int length); + + public virtual void Write(STUNBinaryWriter binary) + { + binary.Write((ushort)GetAttribute(GetType())); + var lengthPos = binary.BaseStream.Position; + binary.Write((ushort)0); + var bodyPos = binary.BaseStream.Position; + WriteBody(binary); + var length = binary.BaseStream.Position - bodyPos; + var endPos = binary.BaseStream.Position; + binary.BaseStream.Position = lengthPos; + binary.Write((ushort)length); + binary.BaseStream.Position = endPos; + } + + public abstract void WriteBody(STUNBinaryWriter binary); + + public static void AddAttribute(int type) where T : STUNAttribute + { + _knownAttributes.Add(type, typeof(T)); + _knownTypes.Add(typeof(T), type); + } + + public static Type GetAttribute(int attribute) + { + Type type; + + if (_knownAttributes.TryGetValue(attribute, out type)) + { + return type; + } + + return null; + } + + public static int GetAttribute(Type type) + { + int attr; + + if (_knownTypes.TryGetValue(type, out attr)) + { + return attr; + } + + return -1; + } + } +} diff --git a/Fika.Core/Networking/STUN/STUNAttributeTypes.cs b/Fika.Core/Networking/STUN/STUNAttributeTypes.cs new file mode 100644 index 00000000..640a9348 --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNAttributeTypes.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN +{ + public enum StunAttributeTypes : ushort + { + MappedAddress, + ResponseAddress, + ChangeRequest, + SourceAddress, + ChangedAddress, + Username, + Password, + MessageIntegrity, + ErrorCode, + UnknownAttributes, + ReflectedFrom, + } +} diff --git a/Fika.Core/Networking/STUN/STUNBinaryReader.cs b/Fika.Core/Networking/STUN/STUNBinaryReader.cs new file mode 100644 index 00000000..b3bd7fa1 --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNBinaryReader.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; + +namespace STUN +{ + public class STUNBinaryReader : BinaryReader + { + public STUNBinaryReader(Stream stream) : base(stream) + { + + } + + public override short ReadInt16() + { + return BitConverter.ToInt16(ReadNetworkBytes(sizeof(short)), 0); + } + + public override ushort ReadUInt16() + { + return BitConverter.ToUInt16(ReadNetworkBytes(sizeof(ushort)), 0); + } + + public override int ReadInt32() + { + return BitConverter.ToInt32(ReadNetworkBytes(sizeof(int)), 0); + } + + public override uint ReadUInt32() + { + return BitConverter.ToUInt32(ReadNetworkBytes(sizeof(uint)), 0); + } + + public override long ReadInt64() + { + return BitConverter.ToInt64(ReadNetworkBytes(sizeof(long)), 0); + } + + public override ulong ReadUInt64() + { + return BitConverter.ToUInt64(ReadNetworkBytes(sizeof(ulong)), 0); + } + + public byte[] ReadNetworkBytes(int count) + { + var bytes = base.ReadBytes(count); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + return bytes; + } + } +} diff --git a/Fika.Core/Networking/STUN/STUNBinaryWriter.cs b/Fika.Core/Networking/STUN/STUNBinaryWriter.cs new file mode 100644 index 00000000..2c68364a --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNBinaryWriter.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; + +namespace STUN +{ + public class STUNBinaryWriter : BinaryWriter + { + public STUNBinaryWriter(Stream stream) : base(stream) + { + + } + + public override void Write(short value) + { + WriteNetworkBytes(BitConverter.GetBytes(value)); + } + + public override void Write(ushort value) + { + WriteNetworkBytes(BitConverter.GetBytes(value)); + } + + public override void Write(int value) + { + WriteNetworkBytes(BitConverter.GetBytes(value)); + } + + public override void Write(uint value) + { + WriteNetworkBytes(BitConverter.GetBytes(value)); + } + + public override void Write(long value) + { + WriteNetworkBytes(BitConverter.GetBytes(value)); + } + + public override void Write(ulong value) + { + WriteNetworkBytes(BitConverter.GetBytes(value)); + } + + private void WriteNetworkBytes(byte[] buffer) + { + if (BitConverter.IsLittleEndian) + { + Array.Reverse(buffer); + } + + base.Write(buffer); + } + } +} diff --git a/Fika.Core/Networking/STUN/STUNClient.cs b/Fika.Core/Networking/STUN/STUNClient.cs new file mode 100644 index 00000000..280a2cce --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNClient.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Collections; +using STUN.Attributes; +using System.IO; + +namespace STUN +{ + /// + /// Implements a RFC3489 STUN client. + /// + public static class STUNClient + { + /// + /// Period of time in miliseconds to wait for server response. + /// + public static int ReceiveTimeout = 5000; + + /// Server address + /// Query type + /// + /// Set to true if created socket should closed after the query + /// else will leave open and can be used. + /// + public static Task QueryAsync(IPEndPoint server, STUNQueryType queryType, bool closeSocket) + { + return Task.Run(() => Query(server, queryType, closeSocket)); + } + + /// A UDP that will use for query. You can also use + /// Server address + /// Query type + public static Task QueryAsync(Socket socket, IPEndPoint server, STUNQueryType queryType, + NATTypeDetectionRFC natTypeDetectionRfc) + { + return Task.Run(() => Query(socket, server, queryType, natTypeDetectionRfc)); + } + + /// Server address + /// Query type + /// + /// Set to true if created socket should closed after the query + /// else will leave open and can be used. + /// + public static STUNQueryResult Query(IPEndPoint server, STUNQueryType queryType, bool closeSocket, + NATTypeDetectionRFC natTypeDetectionRfc = NATTypeDetectionRFC.Rfc3489) + { + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + IPEndPoint bindEndPoint = new IPEndPoint(IPAddress.Any, 0); + socket.Bind(bindEndPoint); + + var result = Query(socket, server, queryType, natTypeDetectionRfc); + + if (closeSocket) + { + socket.Dispose(); + result.Socket = null; + } + + return result; + } + + /// A UDP that will use for query. You can also use + /// Server address + /// Query type + /// Rfc algorithm type + public static STUNQueryResult Query(Socket socket, IPEndPoint server, STUNQueryType queryType, + NATTypeDetectionRFC natTypeDetectionRfc) + { + if (natTypeDetectionRfc == NATTypeDetectionRFC.Rfc3489) + { + return STUNRfc3489.Query(socket, server, queryType, ReceiveTimeout); + } + + if (natTypeDetectionRfc == NATTypeDetectionRFC.Rfc5780) + { + return STUNRfc5780.Query(socket, server, queryType, ReceiveTimeout); + } + + return new STUNQueryResult(); + } + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/STUN/STUNErrorCode.cs b/Fika.Core/Networking/STUN/STUNErrorCode.cs new file mode 100644 index 00000000..a32d15a3 --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNErrorCode.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN +{ + public enum STUNErrorCodes : ushort + { + BadRequest = 400, + Unauthorized = 401, + UnknownAttribute = 420, + StaleCredentials = 430, + IntegrityCheckFailure = 431, + MissingUsername = 432, + UseTLS = 433, + ServerError = 500, + GloablFailure = 600, + } +} diff --git a/Fika.Core/Networking/STUN/STUNMessage.cs b/Fika.Core/Networking/STUN/STUNMessage.cs new file mode 100644 index 00000000..5e370faa --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNMessage.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Net; +using System.IO; + +namespace STUN +{ + public class STUNMessage + { + public STUNMessageTypes MessageType { get; set; } + public byte[] TransactionID { get; set; } + public List Attributes { get; set; } + + public STUNMessage(STUNMessageTypes messageType, byte[] transactionID) + { + MessageType = messageType; + TransactionID = transactionID; + Attributes = new List(); + } + + public bool TryParse(byte[] buffer) + { + try + { + Parse(buffer); + return true; + } + catch + { + return false; + } + } + + public void Parse(byte[] buffer) + { + Parse(buffer, 0, buffer.Length); + } + + public void Parse(byte[] buffer, int index, int count) + { + using (MemoryStream memory = new MemoryStream(buffer, index, count)) + using (STUNBinaryReader binary = new STUNBinaryReader(memory)) + { + Parse(binary); + } + } + + public void Parse(STUNBinaryReader binary) + { + MessageType = (STUNMessageTypes)binary.ReadUInt16(); + int messageLength = binary.ReadUInt16(); + TransactionID = binary.ReadBytes(16); + + Attributes = new List(); + + int attrType; + int attrLength; + int paddingLength; + + while ((binary.BaseStream.Position - 20) < messageLength) + { + attrType = binary.ReadUInt16(); + attrLength = binary.ReadUInt16(); + + if (attrLength % 4 == 0) + { + paddingLength = 0; + } + else + { + paddingLength = 4 - attrLength % 4; + } + + var type = STUNAttribute.GetAttribute(attrType); + + if (type != null) + { + var attr = Activator.CreateInstance(type) as STUNAttribute; + attr.Parse(binary, attrLength); + Attributes.Add(attr); + } + else + { + binary.BaseStream.Position += attrLength; + } + + binary.BaseStream.Position += paddingLength; + } + } + + public void Write(Stream stream) + { + using (STUNBinaryWriter binary = new STUNBinaryWriter(stream)) + { + Write(binary); + } + } + + public void Write(STUNBinaryWriter binary) + { + binary.Write((ushort)MessageType); + binary.Write((ushort)0); + binary.Write(TransactionID); + + long length = 0; + + foreach (var attribute in Attributes) + { + var startPos = binary.BaseStream.Position; + attribute.Write(binary); + length += binary.BaseStream.Position - startPos; + } + + binary.BaseStream.Position = 2; + binary.Write((ushort)length); + } + + public byte[] GetBytes() + { + using (MemoryStream memory = new MemoryStream()) + { + Write(memory); + return memory.ToArray(); + } + } + + public static byte[] GenerateTransactionIDNewStun() + { + Guid guid = Guid.NewGuid(); + var guidArray = guid.ToByteArray(); + // Add magic_cookie as a part of transaction id + byte[] guidByte = new byte[16]; + guidByte[0] = 0x21; + guidByte[1] = 0x12; + guidByte[2] = 0xA4; + guidByte[3] = 0x42; + Buffer.BlockCopy(guidArray, 0,guidByte, 4, 12); + return guidByte; + } + + public static byte[] GenerateTransactionID() + { + Guid guid = Guid.NewGuid(); + return guid.ToByteArray(); + } + } +} diff --git a/Fika.Core/Networking/STUN/STUNMessageTypes.cs b/Fika.Core/Networking/STUN/STUNMessageTypes.cs new file mode 100644 index 00000000..3f8f0460 --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNMessageTypes.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace STUN +{ + public enum STUNMessageTypes : ushort + { + BindingRequest = 0x0001, + BindingResponse = 0x0101, + BindingErrorResponse = 0x0111, + SharedSecretRequest = 0x0002, + SharedSecretResponse = 0x0102, + SharedSecretErrorResponse = 0x0112 + } +} diff --git a/Fika.Core/Networking/STUN/STUNNATType.cs b/Fika.Core/Networking/STUN/STUNNATType.cs new file mode 100644 index 00000000..62603925 --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNNATType.cs @@ -0,0 +1,43 @@ +namespace STUN +{ + public enum STUNNATType + { + /// + /// Unspecified NAT Type + /// + Unspecified, + + /// + /// Open internet. for example Virtual Private Servers. + /// + OpenInternet, + + /// + /// Full Cone NAT. Good to go. + /// + FullCone, + + /// + /// Restricted Cone NAT. + /// It mean's client can only receive data only IP addresses that it sent a data before. + /// + Restricted, + + /// + /// Port-Restricted Cone NAT. + /// Same as but port is included too. + /// + PortRestricted, + + /// + /// Symmetric NAT. + /// It's means the client pick's a different port for every connection it made. + /// + Symmetric, + + /// + /// Same as but only received data from addresses that it sent a data before. + /// + SymmetricUDPFirewall, + } +} diff --git a/Fika.Core/Networking/STUN/STUNNatFilteringBehavior.cs b/Fika.Core/Networking/STUN/STUNNatFilteringBehavior.cs new file mode 100644 index 00000000..e67d2ec1 --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNNatFilteringBehavior.cs @@ -0,0 +1,9 @@ +namespace STUN +{ + public enum STUNNatFilteringBehavior + { + EndpointIndependentFiltering, + AddressDependFiltering, + AddressAndPortDependFiltering + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/STUN/STUNNatMappingBehavior.cs b/Fika.Core/Networking/STUN/STUNNatMappingBehavior.cs new file mode 100644 index 00000000..861393ff --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNNatMappingBehavior.cs @@ -0,0 +1,9 @@ +namespace STUN +{ + public enum STUNNatMappingBehavior + { + EndpointIndependentMapping, + AddressDependMapping, + AddressAndPortDependMapping + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/STUN/STUNQueryError.cs b/Fika.Core/Networking/STUN/STUNQueryError.cs new file mode 100644 index 00000000..77db035a --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNQueryError.cs @@ -0,0 +1,35 @@ +namespace STUN +{ + public enum STUNQueryError + { + /// + /// Indicates querying was successful. + /// + Success, + + /// + /// Indicates the server responsed with error. + /// In this case you have check and in query result. + /// + ServerError, + + /// + /// Indicates the server responsed a bad\wrong\.. message. This error will returned in many cases. + /// + BadResponse, + + /// + /// Indicates the server responsed a message that contains a different transcation ID + /// + BadTransactionID, + + /// + /// Indicates the server didn't response a request within a time interval + /// + Timedout, + /// + /// Indicates the server did not support nat detection + /// + NotSupported + } +} diff --git a/Fika.Core/Networking/STUN/STUNQueryResult.cs b/Fika.Core/Networking/STUN/STUNQueryResult.cs new file mode 100644 index 00000000..8d75c5d2 --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNQueryResult.cs @@ -0,0 +1,59 @@ +using System.Net; +using System.Net.Sockets; + +namespace STUN +{ + /// + /// STUN client query result + /// + public class STUNQueryResult + { + /// + /// The query type that passed to method + /// + public STUNQueryType QueryType { get; set; } + + /// + /// The query result error + /// + public STUNQueryError QueryError { get; set; } + + /// + /// Contains the server error code that receive from server. + /// Presents if set too + /// + public STUNErrorCodes ServerError { get; set; } + + /// + /// Contains the server error phrase that receive from server. + /// Presents if set to + /// + public string ServerErrorPhrase { get; set; } + + /// + /// The socket that used to communicate with STUN server + /// + public Socket Socket { get; set; } + + /// + /// Contains the server address + /// + public IPEndPoint ServerEndPoint { get; set; } + + /// + /// Contains the queried NAT Type. + /// Presents if set to + /// + public STUNNATType NATType { get; set; } + + /// + /// Contains the public endpoint that queried from server. + /// + public IPEndPoint PublicEndPoint { get; set; } + + /// + /// Contains client's socket local endpoiont. + /// + public IPEndPoint LocalEndPoint { get; set; } + } +} diff --git a/Fika.Core/Networking/STUN/STUNQueryType.cs b/Fika.Core/Networking/STUN/STUNQueryType.cs new file mode 100644 index 00000000..a2d00362 --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNQueryType.cs @@ -0,0 +1,25 @@ +namespace STUN +{ + public enum STUNQueryType + { + /// + /// Indicates to client to just query IP address and return + /// + PublicIP, + + /// + /// Indicates to client to stop the querying if NAT type is strict. + /// If the NAT is strict the NAT type will set too + /// Else the NAT type will set to one of following types + /// + /// + /// + /// + OpenNAT, + + /// + /// Indicates to client to find the exact NAT type. + /// + ExactNAT, + } +} diff --git a/Fika.Core/Networking/STUN/STUNRfc3489.cs b/Fika.Core/Networking/STUN/STUNRfc3489.cs new file mode 100644 index 00000000..7963f41d --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNRfc3489.cs @@ -0,0 +1,353 @@ +using System.Linq; +using System.Net; +using System.Net.Sockets; +using STUN.Attributes; + +namespace STUN.Attributes +{ + public class STUNRfc3489 + { + public static STUNQueryResult Query(Socket socket, IPEndPoint server, STUNQueryType queryType, int ReceiveTimeout) + { + var result = new STUNQueryResult(); // the query result + result.Socket = socket; + result.ServerEndPoint = server; + result.NATType = STUNNATType.Unspecified; + result.QueryType = queryType; + var transID = STUNMessage.GenerateTransactionID(); // get a random trans id + var message = new STUNMessage(STUNMessageTypes.BindingRequest, transID); // create a bind request + // send the request to server + socket.SendTo(message.GetBytes(), server); + // we set result local endpoint after calling SendTo, + // because if socket is unbound, the system will bind it after SendTo call. + result.LocalEndPoint = socket.LocalEndPoint as IPEndPoint; + + // wait for response + var responseBuffer = STUNUtils.Receive(socket, ReceiveTimeout); + + // didn't receive anything + if (responseBuffer == null) + { + result.QueryError = STUNQueryError.Timedout; + return result; + } + + // try to parse message + if (!message.TryParse(responseBuffer)) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + // check trans id + if (!STUNUtils.ByteArrayCompare(message.TransactionID, transID)) + { + result.QueryError = STUNQueryError.BadTransactionID; + return result; + } + + // finds error-code attribute, used in case of binding error + var errorAttr = message.Attributes.FirstOrDefault(p => p is STUNErrorCodeAttribute) + as STUNErrorCodeAttribute; + + // if server responsed our request with error + if (message.MessageType == STUNMessageTypes.BindingErrorResponse) + { + if (errorAttr == null) + { + // we count a binding error without error-code attribute as bad response (no?) + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + result.QueryError = STUNQueryError.ServerError; + result.ServerError = errorAttr.Error; + result.ServerErrorPhrase = errorAttr.Phrase; + return result; + } + + // return if receive something else binding response + if (message.MessageType != STUNMessageTypes.BindingResponse) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + // not used for now. + var changedAddr = + message.Attributes.FirstOrDefault(p => p is STUNChangedAddressAttribute) as + STUNChangedAddressAttribute; + + // find mapped address attribue in message + // this attribue should present + var mappedAddressAttr = message.Attributes.FirstOrDefault(p => p is STUNMappedAddressAttribute) + as STUNMappedAddressAttribute; + if (mappedAddressAttr == null) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + else + { + result.PublicEndPoint = mappedAddressAttr.EndPoint; + } + + // stop querying and return the public ip if user just wanted to know public ip + if (queryType == STUNQueryType.PublicIP) + { + result.QueryError = STUNQueryError.Success; + return result; + } + + // if our local ip and port equals to mapped address + if (mappedAddressAttr.EndPoint.Equals(socket.LocalEndPoint)) + { + // we send to a binding request again but with change-request attribute + // that tells to server to response us with different endpoint + message = new STUNMessage(STUNMessageTypes.BindingRequest, transID); + message.Attributes.Add(new STUNChangeRequestAttribute(true, true)); + + socket.SendTo(message.GetBytes(), server); + responseBuffer = STUNUtils.Receive(socket, ReceiveTimeout); + + // if we didnt receive a response + if (responseBuffer == null) + { + result.QueryError = STUNQueryError.Success; + result.NATType = STUNNATType.SymmetricUDPFirewall; + return result; + } + + if (!message.TryParse(responseBuffer)) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + if (!STUNUtils.ByteArrayCompare(message.TransactionID, transID)) + { + result.QueryError = STUNQueryError.BadTransactionID; + return result; + } + + if (message.MessageType == STUNMessageTypes.BindingResponse) + { + result.QueryError = STUNQueryError.Success; + result.NATType = STUNNATType.OpenInternet; + return result; + } + + if (message.MessageType == STUNMessageTypes.BindingErrorResponse) + { + errorAttr = + message.Attributes.FirstOrDefault(p => p is STUNErrorCodeAttribute) as + STUNErrorCodeAttribute; + + if (errorAttr == null) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + result.QueryError = STUNQueryError.ServerError; + result.ServerError = errorAttr.Error; + result.ServerErrorPhrase = errorAttr.Phrase; + return result; + } + + // the message type is wrong + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + message = new STUNMessage(STUNMessageTypes.BindingRequest, transID); + message.Attributes.Add(new STUNChangeRequestAttribute(true, true)); + + var testmsg = new STUNMessage(STUNMessageTypes.BindingRequest, null); + testmsg.Parse(message.GetBytes()); + + socket.SendTo(message.GetBytes(), server); + + responseBuffer = STUNUtils.Receive(socket, ReceiveTimeout); + + if (responseBuffer != null) + { + if (!message.TryParse(responseBuffer)) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + if (!STUNUtils.ByteArrayCompare(message.TransactionID, transID)) + { + result.QueryError = STUNQueryError.BadTransactionID; + return result; + } + + if (message.MessageType == STUNMessageTypes.BindingResponse) + { + result.QueryError = STUNQueryError.Success; + result.NATType = STUNNATType.FullCone; + return result; + } + + if (message.MessageType == STUNMessageTypes.BindingErrorResponse) + { + errorAttr = + message.Attributes.FirstOrDefault(p => p is STUNErrorCodeAttribute) as + STUNErrorCodeAttribute; + + if (errorAttr == null) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + result.QueryError = STUNQueryError.ServerError; + result.ServerError = errorAttr.Error; + result.ServerErrorPhrase = errorAttr.Phrase; + return result; + } + + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + // if user only wanted to know the NAT is open or not + if (queryType == STUNQueryType.OpenNAT) + { + result.QueryError = STUNQueryError.Success; + result.NATType = STUNNATType.Unspecified; + return result; + } + + // we now need changed-address attribute + // because we send our request to this address instead of the first server address + if (changedAddr == null) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + else + { + server = changedAddr.EndPoint; + } + + message = new STUNMessage(STUNMessageTypes.BindingRequest, transID); + socket.SendTo(message.GetBytes(), server); + + responseBuffer = STUNUtils.Receive(socket, ReceiveTimeout); + + if (responseBuffer == null) + { + result.QueryError = STUNQueryError.Timedout; + return result; + } + + if (!message.TryParse(responseBuffer)) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + if (!STUNUtils.ByteArrayCompare(message.TransactionID, transID)) + { + result.QueryError = STUNQueryError.BadTransactionID; + return result; + } + + errorAttr = + message.Attributes.FirstOrDefault(p => p is STUNErrorCodeAttribute) as STUNErrorCodeAttribute; + + if (message.MessageType == STUNMessageTypes.BindingErrorResponse) + { + if (errorAttr == null) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + result.QueryError = STUNQueryError.ServerError; + result.ServerError = errorAttr.Error; + result.ServerErrorPhrase = errorAttr.Phrase; + return result; + } + + if (message.MessageType != STUNMessageTypes.BindingResponse) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + mappedAddressAttr = message.Attributes.FirstOrDefault(p => p is STUNMappedAddressAttribute) + as STUNMappedAddressAttribute; + + if (mappedAddressAttr == null) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + if (!mappedAddressAttr.EndPoint.Equals(result.PublicEndPoint)) + { + result.QueryError = STUNQueryError.Success; + result.NATType = STUNNATType.Symmetric; + result.PublicEndPoint = null; + return result; + } + + message = new STUNMessage(STUNMessageTypes.BindingRequest, transID); + message.Attributes.Add(new STUNChangeRequestAttribute(false, true)); // change port but not ip + + socket.SendTo(message.GetBytes(), server); + + responseBuffer = STUNUtils.Receive(socket, ReceiveTimeout); + + if (responseBuffer == null) + { + result.QueryError = STUNQueryError.Success; + result.NATType = STUNNATType.PortRestricted; + return result; + } + + if (!message.TryParse(responseBuffer)) + { + result.QueryError = STUNQueryError.Timedout; + return result; + } + + if (!STUNUtils.ByteArrayCompare(message.TransactionID, transID)) + { + result.QueryError = STUNQueryError.BadTransactionID; + return result; + } + + errorAttr = message.Attributes.FirstOrDefault(p => p is STUNErrorCodeAttribute) + as STUNErrorCodeAttribute; + + if (message.MessageType == STUNMessageTypes.BindingErrorResponse) + { + if (errorAttr == null) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + result.QueryError = STUNQueryError.ServerError; + result.ServerError = errorAttr.Error; + result.ServerErrorPhrase = errorAttr.Phrase; + return result; + } + + if (message.MessageType != STUNMessageTypes.BindingResponse) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + result.QueryError = STUNQueryError.Success; + result.NATType = STUNNATType.Restricted; + return result; + } + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/STUN/STUNRfc5780.cs b/Fika.Core/Networking/STUN/STUNRfc5780.cs new file mode 100644 index 00000000..ff6d112b --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNRfc5780.cs @@ -0,0 +1,251 @@ +using System.Linq; +using System.Net; +using System.Net.Sockets; +using STUN.Attributes; + +namespace STUN +{ + public class STUNRfc5780 + { + public static STUNQueryResult Query(Socket socket, IPEndPoint server, STUNQueryType queryType, int ReceiveTimeout) + { + STUNNatMappingBehavior mappingBehavior = STUNNatMappingBehavior.EndpointIndependentMapping; + STUNNatFilteringBehavior filteringBehavior = STUNNatFilteringBehavior.EndpointIndependentFiltering; + var result = new STUNQueryResult(); // the query result + result.Socket = socket; + result.ServerEndPoint = server; + result.NATType = STUNNATType.Unspecified; + result.QueryType = queryType; + + var transID = STUNMessage.GenerateTransactionIDNewStun(); // get a random trans id + var message = new STUNMessage(STUNMessageTypes.BindingRequest, transID); // create a bind request + // send the request to server + socket.SendTo(message.GetBytes(), server); + // we set result local endpoint after calling SendTo, + // because if socket is unbound, the system will bind it after SendTo call. + result.LocalEndPoint = socket.LocalEndPoint as IPEndPoint; + + // wait for response + var responseBuffer = STUNUtils.Receive(socket, ReceiveTimeout); + + // didn't receive anything + if (responseBuffer == null) + { + result.QueryError = STUNQueryError.Timedout; + return result; + } + + // try to parse message + if (!message.TryParse(responseBuffer)) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + // check trans id + if (!STUNUtils.ByteArrayCompare(message.TransactionID, transID)) + { + result.QueryError = STUNQueryError.BadTransactionID; + return result; + } + + // finds error-code attribute, used in case of binding error + var errorAttr = message.Attributes.FirstOrDefault(p => p is STUNErrorCodeAttribute) + as STUNErrorCodeAttribute; + + // if server responsed our request with error + if (message.MessageType == STUNMessageTypes.BindingErrorResponse) + { + if (errorAttr == null) + { + // we count a binding error without error-code attribute as bad response (no?) + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + result.QueryError = STUNQueryError.ServerError; + result.ServerError = errorAttr.Error; + result.ServerErrorPhrase = errorAttr.Phrase; + return result; + } + + // return if receive something else binding response + if (message.MessageType != STUNMessageTypes.BindingResponse) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + var xorAddressAttribute = message.Attributes.FirstOrDefault(p => p is STUNXorMappedAddressAttribute) + as STUNXorMappedAddressAttribute; + + if (xorAddressAttribute == null) + { + result.QueryError = STUNQueryError.BadResponse; + return result; + } + + result.PublicEndPoint = xorAddressAttribute.EndPoint; + + // stop querying and return the public ip if user just wanted to know public ip + if (queryType == STUNQueryType.PublicIP) + { + result.QueryError = STUNQueryError.Success; + return result; + } + + + if (xorAddressAttribute.EndPoint.Equals(socket.LocalEndPoint)) + { + result.NATType = STUNNATType.OpenInternet; + } + + var otherAddressAttribute = message.Attributes.FirstOrDefault(p => p is STUNOtherAddressAttribute) + as STUNOtherAddressAttribute; + + var changedAddressAttribute = message.Attributes.FirstOrDefault(p => p is STUNChangedAddressAttribute) + as STUNChangedAddressAttribute; + // Check is next test should be performed and is support rfc5780 test + if (otherAddressAttribute == null) + { + if (changedAddressAttribute == null) + { + result.QueryError = STUNQueryError.NotSupported; + return result; + } + + otherAddressAttribute = new STUNOtherAddressAttribute(); + otherAddressAttribute.EndPoint = changedAddressAttribute.EndPoint; + } + + + // Make test 2 - bind different ip address but primary port + message = new STUNMessage(STUNMessageTypes.BindingRequest, transID); // create a bind request + IPEndPoint secondaryServer = new IPEndPoint(otherAddressAttribute.EndPoint.Address, server.Port); + socket.SendTo(message.GetBytes(), secondaryServer); + responseBuffer = STUNUtils.Receive(socket, ReceiveTimeout); + + // Secondary server presented but is down + if (responseBuffer == null) + { + result.QueryError = STUNQueryError.NotSupported; + return result; + } + + if (!message.TryParse(responseBuffer)) + { + result.QueryError = STUNQueryError.NotSupported; + return result; + } + + var xorAddressAttribute2 = message.Attributes.FirstOrDefault(p => p is STUNXorMappedAddressAttribute) + as STUNXorMappedAddressAttribute; + + if (xorAddressAttribute2 != null) + { + if (xorAddressAttribute.EndPoint.Equals(xorAddressAttribute2.EndPoint)) + { + mappingBehavior = STUNNatMappingBehavior.EndpointIndependentMapping; + } + + // Make test 3 + else + { + IPEndPoint secondaryServerPort = new IPEndPoint(otherAddressAttribute.EndPoint.Address, + otherAddressAttribute.EndPoint.Port); + + message = new STUNMessage(STUNMessageTypes.BindingRequest, transID); // create a bind request + socket.SendTo(message.GetBytes(), secondaryServerPort); + responseBuffer = STUNUtils.Receive(socket, ReceiveTimeout); + + if (!message.TryParse(responseBuffer)) + { + result.QueryError = STUNQueryError.NotSupported; + return result; + } + + var xorAddressAttribute3 = + message.Attributes.FirstOrDefault(p => p is STUNXorMappedAddressAttribute) + as STUNXorMappedAddressAttribute; + + if (xorAddressAttribute3 != null) + { + if (xorAddressAttribute3.EndPoint.Equals(xorAddressAttribute2.EndPoint)) + { + mappingBehavior = STUNNatMappingBehavior.AddressDependMapping; + } + + else + { + mappingBehavior = STUNNatMappingBehavior.AddressAndPortDependMapping; + } + } + } + } + + // Now make a filtering behavioral test + // We already made a test 1 for mapping behavioral + // so jump to test 2 + + // Send message to primary server. + // Try receive from another server and port + message = new STUNMessage(STUNMessageTypes.BindingRequest, transID); + message.Attributes.Add(new STUNChangeRequestAttribute(true, true)); + + socket.SendTo(message.GetBytes(), server); + + responseBuffer = STUNUtils.Receive(socket, ReceiveTimeout); + + if (responseBuffer != null) + { + filteringBehavior = STUNNatFilteringBehavior.EndpointIndependentFiltering; + } + + // Test 3 - send request to original server with change port attribute + else + { + message = new STUNMessage(STUNMessageTypes.BindingRequest, transID); + message.Attributes.Add(new STUNChangeRequestAttribute(false, true)); + + socket.SendTo(message.GetBytes(), server); + + responseBuffer = STUNUtils.Receive(socket, ReceiveTimeout); + + if (responseBuffer != null) + { + filteringBehavior = STUNNatFilteringBehavior.AddressDependFiltering; + } + + else + { + filteringBehavior = STUNNatFilteringBehavior.AddressAndPortDependFiltering; + } + } + + if (filteringBehavior == STUNNatFilteringBehavior.AddressAndPortDependFiltering && + mappingBehavior == STUNNatMappingBehavior.AddressAndPortDependMapping) + { + result.NATType = STUNNATType.Symmetric; + } + + if (filteringBehavior == STUNNatFilteringBehavior.EndpointIndependentFiltering && + mappingBehavior == STUNNatMappingBehavior.EndpointIndependentMapping) + { + result.NATType = STUNNATType.FullCone; + } + + if (filteringBehavior == STUNNatFilteringBehavior.EndpointIndependentFiltering && + mappingBehavior == STUNNatMappingBehavior.AddressDependMapping) + { + result.NATType = STUNNATType.Restricted; + } + + if (result.NATType == STUNNATType.Unspecified) + { + result.NATType = STUNNATType.PortRestricted; + } + + return result; + } + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/STUN/STUNUtils.cs b/Fika.Core/Networking/STUN/STUNUtils.cs new file mode 100644 index 00000000..2106124c --- /dev/null +++ b/Fika.Core/Networking/STUN/STUNUtils.cs @@ -0,0 +1,80 @@ +using System.Linq; +using System.Net; +using System.Net.Sockets; + +namespace STUN.Attributes +{ + public class STUNUtils + { + public static byte[] Receive(Socket socket, int timeout) + { + if (!socket.Poll(timeout * 1000, SelectMode.SelectRead)) + { + return null; + } + + EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); + + byte[] buffer = new byte[1024 * 2]; + int bytesRead = 0; + + bytesRead = socket.ReceiveFrom(buffer, ref endPoint); + + return buffer.Take(bytesRead).ToArray(); + } + + public static bool TryParseHostAndPort(string hostAndPort, out IPEndPoint endPoint) + { + if (string.IsNullOrWhiteSpace(hostAndPort)) + { + endPoint = null; + return false; + } + + var split = hostAndPort.Split(':'); + + if (split.Length != 2) + { + endPoint = null; + return false; + } + + if (!ushort.TryParse(split[1], out ushort port)) + { + endPoint = null; + return false; + } + + if (!IPAddress.TryParse(split[0], out IPAddress address)) + { + try + { +#if NETSTANDARD1_3 + address = Dns.GetHostEntryAsync(split[0]).GetAwaiter().GetResult().AddressList.First(); +#else + address = Dns.GetHostEntry(split[0]).AddressList.First(); +#endif + } + catch + { + endPoint = null; + return false; + } + } + + endPoint = new IPEndPoint(address, port); + return true; + } + + public static bool ByteArrayCompare(byte[] b1, byte[] b2) + { + if (b1 == b2) + return true; + + if (b1.Length != b2.Length) + return false; + + return b1.SequenceEqual(b2); + } + } +} \ No newline at end of file