diff --git a/Fika.Core/Coop/GameMode/CoopGame.cs b/Fika.Core/Coop/GameMode/CoopGame.cs index 71ac82e6..8a58f7cb 100644 --- a/Fika.Core/Coop/GameMode/CoopGame.cs +++ b/Fika.Core/Coop/GameMode/CoopGame.cs @@ -9,7 +9,6 @@ using EFT.Counters; using EFT.EnvironmentEffect; using EFT.Game.Spawning; -using EFT.HealthSystem; using EFT.Interactive; using EFT.InventoryLogic; using EFT.UI; @@ -29,6 +28,7 @@ using Fika.Core.Networking; using Fika.Core.Networking.Http; using Fika.Core.Networking.Http.Models; +using Fika.Core.Networking.NatPunch; using Fika.Core.Networking.Packets.GameWorld; using Fika.Core.UI.Models; using HarmonyLib; @@ -924,6 +924,16 @@ public override async Task vmethod_2(int playerId, Vector3 position await WaitForPlayers(); + if(isServer && FikaPlugin.UseNatPunching.Value) + { + FikaNatPunchServer natPunchServer = Singleton.Instance.FikaNatPunchServer; + + if (natPunchServer != null && natPunchServer.Connected) + { + natPunchServer.Close(); + } + } + fikaDebug = gameObject.AddComponent(); Destroy(customButton); diff --git a/Fika.Core/Coop/Utils/FikaBackendUtils.cs b/Fika.Core/Coop/Utils/FikaBackendUtils.cs index f418f333..4504538f 100644 --- a/Fika.Core/Coop/Utils/FikaBackendUtils.cs +++ b/Fika.Core/Coop/Utils/FikaBackendUtils.cs @@ -30,6 +30,8 @@ public static class FikaBackendUtils public static WeatherClass[] Nodes = null; public static string RemoteIp; public static int RemotePort; + public static int LocalPort = 0; + public static bool IsHostNatPunch = false; private static string groupId; public static MatchmakerTimeHasCome.GClass3187 ScreenController; diff --git a/Fika.Core/Coop/Utils/NetManagerUtils.cs b/Fika.Core/Coop/Utils/NetManagerUtils.cs index d5fa68cd..1a437d53 100644 --- a/Fika.Core/Coop/Utils/NetManagerUtils.cs +++ b/Fika.Core/Coop/Utils/NetManagerUtils.cs @@ -13,13 +13,18 @@ public static class NetManagerUtils private static ManualLogSource logger = BepInEx.Logging.Logger.CreateLogSource("NetManagerUtils"); public static GameObject FikaGameObject; + public static void CreateFikaGameObject() + { + FikaGameObject = new GameObject("FikaGameObject"); + Object.DontDestroyOnLoad(FikaGameObject); + logger.LogInfo("FikaGameObject has been created!"); + } + public static void CreateNetManager(bool isServer) { if (FikaGameObject == null) { - FikaGameObject = new GameObject("FikaGameObject"); - Object.DontDestroyOnLoad(FikaGameObject); - logger.LogInfo("FikaGameObject has been created!"); + CreateFikaGameObject(); } if (isServer) @@ -36,6 +41,18 @@ public static void CreateNetManager(bool isServer) } } + public static void CreatePingingClient() + { + if (FikaGameObject == null) + { + CreateFikaGameObject(); + } + + FikaPingingClient pingingClient = FikaGameObject.AddComponent(); + Singleton.Create(pingingClient); + logger.LogInfo("FikaPingingClient has started!"); + } + public static void DestroyNetManager(bool isServer) { if (FikaGameObject != null) @@ -55,6 +72,17 @@ public static void DestroyNetManager(bool isServer) } } + public static void DestroyPingingClient() + { + if(FikaGameObject != null) + { + Singleton.Instance.StopKeepAliveRoutine(); + Singleton.Instance.NetClient.Stop(); + Singleton.TryRelease(Singleton.Instance); + logger.LogInfo("Destroyed FikaPingingClient"); + } + } + public static Task InitNetManager(bool isServer) { if (FikaGameObject != null) 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/FikaPlugin.cs b/Fika.Core/FikaPlugin.cs index 9c7714c7..53170288 100644 --- a/Fika.Core/FikaPlugin.cs +++ b/Fika.Core/FikaPlugin.cs @@ -167,6 +167,7 @@ public class FikaPlugin : BaseUnityPlugin public static ConfigEntry AutoRefreshRate { get; set; } public static ConfigEntry UDPPort { get; set; } public static ConfigEntry UseUPnP { get; set; } + public static ConfigEntry UseNatPunching { get; set; } public static ConfigEntry ConnectionTimeout { get; set; } // Gameplay @@ -192,6 +193,7 @@ protected void Awake() { Instance = this; + GetClientConfig(); SetupConfig(); new FikaVersionLabel_Patch().Enable(); @@ -227,7 +229,6 @@ protected void Awake() BundleLoaderPlugin.Create(); FikaAirdropUtil.GetConfigFromServer(); - GetClientConfig(); BotSettingsRepoClass.Init(); if (AllowItemSending) @@ -395,17 +396,19 @@ private void SetupConfig() // Network - NativeSockets = Config.Bind(section: "Network", "Native Sockets", false, new ConfigDescription("Use NativeSockets for gameplay traffic. This uses direct socket calls for send/receive to drastically increase speed and reduce GC pressure. Only for Windows/Linux and might not always work.", tags: new ConfigurationManagerAttributes() { Order = 7 })); + NativeSockets = Config.Bind(section: "Network", "Native Sockets", false, new ConfigDescription("Use NativeSockets for gameplay traffic. This uses direct socket calls for send/receive to drastically increase speed and reduce GC pressure. Only for Windows/Linux and might not always work.", tags: new ConfigurationManagerAttributes() { Order = 8 })); + + ForceIP = Config.Bind("Network", "Force IP", "", new ConfigDescription("Forces the server when hosting to use this IP when broadcasting to the backend instead of automatically trying to fetch it. Leave empty to disable.", tags: new ConfigurationManagerAttributes() { Order = 7 })); - ForceIP = Config.Bind("Network", "Force IP", "", new ConfigDescription("Forces the server when hosting to use this IP when broadcasting to the backend instead of automatically trying to fetch it. Leave empty to disable.", tags: new ConfigurationManagerAttributes() { Order = 6 })); + ForceBindIP = Config.Bind("Network", "Force Bind IP", "", new ConfigDescription("Forces the server when hosting to use this local IP when starting the server. Useful if you are hosting on a VPN.", new AcceptableValueList(GetLocalAddresses()), new ConfigurationManagerAttributes() { Order = 6 })); - ForceBindIP = Config.Bind("Network", "Force Bind IP", "", new ConfigDescription("Forces the server when hosting to use this local IP when starting the server. Useful if you are hosting on a VPN.", new AcceptableValueList(GetLocalAddresses()), new ConfigurationManagerAttributes() { Order = 5 })); + AutoRefreshRate = Config.Bind("Network", "Auto Server Refresh Rate", 10f, new ConfigDescription("Every X seconds the client will ask the server for the list of matches while at the lobby screen.", new AcceptableValueRange(3f, 60f), new ConfigurationManagerAttributes() { Order = 5 })); - AutoRefreshRate = Config.Bind("Network", "Auto Server Refresh Rate", 10f, new ConfigDescription("Every X seconds the client will ask the server for the list of matches while at the lobby screen.", new AcceptableValueRange(3f, 60f), new ConfigurationManagerAttributes() { Order = 4 })); + UDPPort = Config.Bind("Network", "UDP Port", 25565, new ConfigDescription("Port to use for UDP gameplay packets.", tags: new ConfigurationManagerAttributes() { Order = 4 })); - UDPPort = Config.Bind("Network", "UDP Port", 25565, new ConfigDescription("Port to use for UDP gameplay packets.", tags: new ConfigurationManagerAttributes() { Order = 3 })); + UseUPnP = Config.Bind("Network", "Use UPnP", false, new ConfigDescription("Attempt to open ports using UPnP. Useful if you cannot open ports yourself but the router supports UPnP.", tags: new ConfigurationManagerAttributes() { Order = 3 })); - UseUPnP = Config.Bind("Network", "Use UPnP", false, new ConfigDescription("Attempt to open ports using UPnP. Useful if you cannot open ports yourself but the router supports UPnP.", tags: new ConfigurationManagerAttributes() { Order = 2 })); + UseNatPunching = Config.Bind("Network", "Use NAT Punching", false, new ConfigDescription("Use NAT punching as a NAT traversal method for hosting a raid. Only works with fullcone NAT type routers. UPnP, Force IP and Force Bind IP are disabled in this mode.", tags: new ConfigurationManagerAttributes() { Order = 2 })); ConnectionTimeout = Config.Bind("Network", "Connection Timeout", 15, new ConfigDescription("How long it takes for a connection to be considered dropped if no packets are received.", new AcceptableValueRange(5, 60), new ConfigurationManagerAttributes() { Order = 1 })); diff --git a/Fika.Core/Networking/FikaClient.cs b/Fika.Core/Networking/FikaClient.cs index 731d7c01..a0ff4c68 100644 --- a/Fika.Core/Networking/FikaClient.cs +++ b/Fika.Core/Networking/FikaClient.cs @@ -107,7 +107,12 @@ public void Init() EnableStatistics = true }; - _netClient.Start(); + if (FikaBackendUtils.IsHostNatPunch) + { + NetManagerUtils.DestroyPingingClient(); + } + + _netClient.Start(FikaBackendUtils.LocalPort); string ip = FikaBackendUtils.RemoteIp; int port = FikaBackendUtils.RemotePort; diff --git a/Fika.Core/Networking/FikaPingingClient.cs b/Fika.Core/Networking/FikaPingingClient.cs index a356577e..a0bae91c 100644 --- a/Fika.Core/Networking/FikaPingingClient.cs +++ b/Fika.Core/Networking/FikaPingingClient.cs @@ -2,23 +2,31 @@ using Fika.Core.Coop.Utils; using Fika.Core.Networking.Http; using Fika.Core.Networking.Http.Models; +using Fika.Core.Networking.NatPunch; using LiteNetLib; using LiteNetLib.Utils; +using SPT.Common.Http; +using System.Collections; using System.Net; using System.Net.Sockets; +using System.Threading.Tasks; +using UnityEngine; namespace Fika.Core.Networking { - internal class FikaPingingClient(string serverId) : INetEventListener + public class FikaPingingClient : MonoBehaviour, INetEventListener { public NetManager NetClient; - private readonly ManualLogSource _logger = Logger.CreateLogSource("Fika.PingingClient"); - private readonly string serverId = serverId; + private readonly ManualLogSource _logger = BepInEx.Logging.Logger.CreateLogSource("Fika.PingingClient"); private IPEndPoint remoteEndPoint; private IPEndPoint localEndPoint; + private IPEndPoint remoteStunEndPoint; + private int localPort = 0; public bool Received = false; + private Coroutine keepAliveRoutine; + private Task natPunchRequestTask; - public bool Init() + public bool Init(string serverId) { NetClient = new(this) { @@ -28,6 +36,8 @@ public bool Init() GetHostRequest body = new(serverId); GetHostResponse result = FikaRequestHandler.GetHost(body); + FikaBackendUtils.IsHostNatPunch = result.NatPunch; + string ip = result.Ips[0]; string localIp = null; if (result.Ips.Length > 1) @@ -54,26 +64,84 @@ public bool Init() localEndPoint = new(IPAddress.Parse(localIp), port); } - NetClient.Start(); + if (FikaBackendUtils.IsHostNatPunch) + { + natPunchRequestTask = Task.Run(() => NatPunchRequest(serverId)); + } + + NetClient.Start(localPort); return true; } - public void PingEndPoint() + public void PingEndPoint(string message) { - if (Received) - { - return; - } - NetDataWriter writer = new(); - writer.Put("fika.hello"); + writer.Put(message); NetClient.SendUnconnectedMessage(writer, remoteEndPoint); if (localEndPoint != null) { NetClient.SendUnconnectedMessage(writer, localEndPoint); } + + if (remoteStunEndPoint != null && natPunchRequestTask.IsCompleted) + { + NetClient.SendUnconnectedMessage(writer, remoteStunEndPoint); + } + } + + public async void NatPunchRequest(string serverId) + { + FikaNatPunchClient fikaNatPunchClient = new FikaNatPunchClient(); + fikaNatPunchClient.Connect(); + + if (!fikaNatPunchClient.Connected) + { + _logger.LogError("Unable to connect to NatPunchRelayService."); + return; + } + + StunIPEndPoint localStunEndPoint = NatPunchUtils.CreateStunEndPoint(); + + if (localStunEndPoint == null) + { + _logger.LogError("Nat Punch Request failed: Stun Endpoint is null."); + return; + } + + GetHostStunRequest getStunRequest = new GetHostStunRequest(serverId, RequestHandler.SessionId, localStunEndPoint.Remote.Address.ToString(), localStunEndPoint.Remote.Port); + GetHostStunResponse getStunResponse = await fikaNatPunchClient.GetHostStun(getStunRequest); + + fikaNatPunchClient.Close(); + + remoteStunEndPoint = new IPEndPoint(IPAddress.Parse(getStunResponse.StunIp), getStunResponse.StunPort); + + localPort = localStunEndPoint.Local.Port; + } + + public void StartKeepAliveRoutine() + { + keepAliveRoutine = StartCoroutine(KeepAlive()); + } + + public void StopKeepAliveRoutine() + { + if(keepAliveRoutine != null) + { + StopCoroutine(keepAliveRoutine); + } + } + + public IEnumerator KeepAlive() + { + while(true) + { + PingEndPoint("fika.keepalive"); + NetClient.PollEvents(); + + yield return new WaitForSeconds(1.0f); + } } public void OnConnectionRequest(ConnectionRequest request) @@ -98,23 +166,22 @@ public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelN public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { - if (Received) - { - return; - } - _logger.LogInfo("Received response from server, parsing..."); - if (reader.TryGetString(out string result)) { - if (result == "fika.hello") - { - Received = true; - FikaBackendUtils.RemoteIp = remoteEndPoint.Address.ToString(); - FikaBackendUtils.RemotePort = remoteEndPoint.Port; - } - else + switch(result) { - _logger.LogError("Data was not as expected"); + case "fika.hello": + Received = true; + FikaBackendUtils.RemoteIp = remoteEndPoint.Address.ToString(); + FikaBackendUtils.RemotePort = remoteEndPoint.Port; + FikaBackendUtils.LocalPort = localPort; + break; + case "fika.keepalive": + // Do nothing + break; + default: + _logger.LogError("Data was not as expected"); + break; } } else diff --git a/Fika.Core/Networking/FikaServer.cs b/Fika.Core/Networking/FikaServer.cs index ea7a55f5..37f8d488 100644 --- a/Fika.Core/Networking/FikaServer.cs +++ b/Fika.Core/Networking/FikaServer.cs @@ -17,6 +17,7 @@ using Fika.Core.Modding.Events; using Fika.Core.Networking.Http; using Fika.Core.Networking.Http.Models; +using Fika.Core.Networking.NatPunch; using Fika.Core.Networking.Packets; using Fika.Core.Networking.Packets.Communication; using Fika.Core.Networking.Packets.GameWorld; @@ -72,6 +73,8 @@ public bool Started } } private FikaChat fikaChat; + public FikaNatPunchServer FikaNatPunchServer; + private CancellationTokenSource StunQueryRoutineCts; public async Task Init() { @@ -111,8 +114,8 @@ public async Task Init() UseNativeSockets = FikaPlugin.NativeSockets.Value, EnableStatistics = true }; - - if (FikaPlugin.UseUPnP.Value) + + if (FikaPlugin.UseUPnP.Value && !FikaPlugin.UseNatPunching.Value) { bool upnpFailed = false; @@ -156,17 +159,47 @@ public async Task Init() } } - if (FikaPlugin.ForceBindIP.Value != "Disabled") + if(FikaPlugin.UseNatPunching.Value) { - _netServer.Start(FikaPlugin.ForceBindIP.Value, "", Port); + FikaNatPunchServer = new FikaNatPunchServer(_netServer); + FikaNatPunchServer.Connect(); + + if (FikaNatPunchServer.Connected) + { + StunQueryRoutineCts = new CancellationTokenSource(); + Task stunQueryRoutine = Task.Run(() => NatPunchUtils.StunQueryRoutine(_netServer, FikaNatPunchServer, StunQueryRoutineCts.Token)); + } + else + { + logger.LogError("Unable to connect to FikaNatPunchRelayService."); + } } else { - _netServer.Start(Port); + if (FikaPlugin.ForceBindIP.Value != "Disabled") + { + _netServer.Start(FikaPlugin.ForceBindIP.Value, "", Port); + } + else + { + _netServer.Start(Port); + } } logger.LogInfo("Started Fika Server"); - NotificationManagerClass.DisplayMessageNotification($"Server started on port {_netServer.LocalPort}.", + + string serverStartedMessage; + + if(FikaPlugin.UseNatPunching.Value) + { + serverStartedMessage = "Server started using Nat Punching."; + } + else + { + serverStartedMessage = $"Server started on port {_netServer.LocalPort}."; + } + + NotificationManagerClass.DisplayMessageNotification(serverStartedMessage, EFT.Communications.ENotificationDurationType.Default, EFT.Communications.ENotificationIconType.EntryPoint); string[] Ips = []; @@ -186,7 +219,7 @@ public async Task Init() iconType: EFT.Communications.ENotificationIconType.Alert); } - SetHostRequest body = new(Ips, Port); + SetHostRequest body = new(Ips, Port, FikaPlugin.UseNatPunching.Value); FikaRequestHandler.UpdateSetHost(body); FikaEventDispatcher.DispatchEvent(new FikaServerCreatedEvent(this)); @@ -760,16 +793,31 @@ public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketRead { if (reader.TryGetString(out string data)) { - if (data == "fika.hello") - { - NetDataWriter resp = new(); - resp.Put(data); - _netServer.SendUnconnectedMessage(resp, remoteEndPoint); - logger.LogInfo("PingingRequest: Correct ping query, sending response"); - } - else + NetDataWriter resp; + + switch (data) { - logger.LogError("PingingRequest: Data was not as expected"); + case "fika.hello": + resp = new(); + resp.Put(data); + _netServer.SendUnconnectedMessage(resp, remoteEndPoint); + logger.LogInfo("PingingRequest: Correct ping query, sending response"); + break; + + case "fika.keepalive": + resp = new(); + resp.Put(data); + _netServer.SendUnconnectedMessage(resp, remoteEndPoint); + + if(!StunQueryRoutineCts.IsCancellationRequested) + { + StunQueryRoutineCts.Cancel(); + } + break; + + default: + logger.LogError("PingingRequest: Data was not as expected"); + break; } } else diff --git a/Fika.Core/Networking/Models/GetHostResponse.cs b/Fika.Core/Networking/Models/GetHostResponse.cs index 1297c88f..929c0068 100644 --- a/Fika.Core/Networking/Models/GetHostResponse.cs +++ b/Fika.Core/Networking/Models/GetHostResponse.cs @@ -11,10 +11,14 @@ public struct GetHostResponse [DataMember(Name = "port")] public int Port; - public GetHostResponse(string[] ips, int port) + [DataMember(Name = "natPunch")] + public bool NatPunch; + + public GetHostResponse(string[] ips, int port, bool natPunch) { Ips = ips; Port = port; + NatPunch = natPunch; } } } \ No newline at end of file diff --git a/Fika.Core/Networking/Models/GetHostStunRequest.cs b/Fika.Core/Networking/Models/GetHostStunRequest.cs new file mode 100644 index 00000000..7ac181c0 --- /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 = "sessionId")] + public string SessionId; + + [DataMember(Name = "serverId")] + public string ServerId; + + [DataMember(Name = "stunIp")] + public string StunIp; + + [DataMember(Name = "stunPort")] + public int StunPort; + + public GetHostStunRequest(string serverId, string sessionId, string stunIp, int stunPort) + { + RequestType = GetType().Name; + SessionId = sessionId; + ServerId = serverId; + 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..da74a9fb --- /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 = "sessionId")] + public string SessionId; + + [DataMember(Name = "StunIp")] + public string StunIp; + + [DataMember(Name = "StunPort")] + public int StunPort; + + public GetHostStunResponse(string sessionId, string stunIp, int stunPort) + { + RequestType = GetType().Name; + SessionId = sessionId; + StunIp = stunIp; + StunPort = stunPort; + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/Models/SetHostRequest.cs b/Fika.Core/Networking/Models/SetHostRequest.cs index 57b9203b..df68b130 100644 --- a/Fika.Core/Networking/Models/SetHostRequest.cs +++ b/Fika.Core/Networking/Models/SetHostRequest.cs @@ -15,11 +15,15 @@ public struct SetHostRequest [DataMember(Name = "port")] public int Port; - public SetHostRequest(string[] ips, int port) + [DataMember(Name = "natPunch")] + public bool NatPunch; + + public SetHostRequest(string[] ips, int port, bool natPunch) { ServerId = CoopHandler.GetServerId(); Ips = ips; Port = port; + NatPunch = natPunch; } } } \ 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..d0af0096 --- /dev/null +++ b/Fika.Core/Networking/NatPunch/FikaNatPunchClient.cs @@ -0,0 +1,124 @@ +using WebSocketSharp; +using System; +using Newtonsoft.Json; +using System.Threading.Tasks; +using Fika.Core.Networking.Http.Models; +using SPT.Common.Http; +using BepInEx.Logging; + +namespace Fika.Core.Networking.NatPunch +{ + public class FikaNatPunchClient + { + private static ManualLogSource logger = BepInEx.Logging.Logger.CreateLogSource("Fika.NatPunchClient"); + public string Host { get; set; } + public string Url { get; set; } + public string SessionId { get; set; } + public bool Connected + { + get + { + return _webSocket.ReadyState == WebSocketState.Open ? true : false; + } + } + + private WebSocket _webSocket; + private TaskCompletionSource _receiveTaskCompletion; + + public FikaNatPunchClient() + { + Host = RequestHandler.Host.Replace("http", "ws"); + SessionId = RequestHandler.SessionId; + Url = $"{Host}/fika/natpunchrelayservice/{SessionId}?"; + + _webSocket = new WebSocket(Url) + { + WaitTime = TimeSpan.FromMinutes(1), + EmitOnPing = true + }; + + _webSocket.OnOpen += WebSocket_OnOpen; + _webSocket.OnError += WebSocket_OnError; + _webSocket.OnMessage += WebSocket_OnMessage; + _webSocket.OnClose += WebSocket_OnClose; + } + + public void Connect() + { + if (_webSocket.ReadyState == WebSocketState.Open) + return; + + _webSocket.Connect(); + } + + public void Close() + { + _webSocket.Close(); + } + + private void WebSocket_OnOpen(object sender, EventArgs e) + { + logger.LogInfo("Connected to FikaNatPunchRelayService 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) + { + logger.LogError($"FikaNatPunchClient Websocket error: {e.Message}"); + _webSocket.Close(); + } + + private void WebSocket_OnClose(object sender, CloseEventArgs e) + { + logger.LogInfo($"Disconnected from FikaNatPunchService as client"); + } + + 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) + { + var result = await SendAndReceiveAsync(getHostStunRequest); + + return result; + } + } +} diff --git a/Fika.Core/Networking/NatPunch/FikaNatPunchServer.cs b/Fika.Core/Networking/NatPunch/FikaNatPunchServer.cs new file mode 100644 index 00000000..1cfd6550 --- /dev/null +++ b/Fika.Core/Networking/NatPunch/FikaNatPunchServer.cs @@ -0,0 +1,153 @@ +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; +using BepInEx.Logging; + +namespace Fika.Core.Networking.NatPunch +{ + public class FikaNatPunchServer + { + private static ManualLogSource logger = BepInEx.Logging.Logger.CreateLogSource("Fika.NatPunchServer"); + + public string Host { get; set; } + public string Url { get; set; } + public string SessionId { get; set; } + public bool Connected + { + get + { + return _webSocket.ReadyState == WebSocketState.Open ? true : false; + } + } + private StunIPEndPoint _stunIpEndPoint; + public StunIPEndPoint StunIPEndpoint { + get { + return _stunIpEndPoint; + } + set { + _stunIpEndPoint = value; + } + } + + private WebSocket _webSocket; + private NetManager _netManager; + + public FikaNatPunchServer(NetManager netManager) + { + Host = RequestHandler.Host.Replace("http", "ws"); + SessionId = RequestHandler.SessionId; + Url = $"{Host}/fika/natpunchrelayservice/{SessionId}?"; + + _webSocket = new WebSocket(Url) + { + WaitTime = TimeSpan.FromMinutes(1), + EmitOnPing = true + }; + + _webSocket.OnOpen += WebSocket_OnOpen; + _webSocket.OnError += WebSocket_OnError; + _webSocket.OnMessage += WebSocket_OnMessage; + _webSocket.OnClose += WebSocket_OnClose; + + _netManager = netManager; + } + + public void Connect() + { + _webSocket.Connect(); + } + + public void Close() + { + _webSocket.Close(); + } + + private void WebSocket_OnOpen(object sender, EventArgs e) + { + logger.LogInfo("Connected to FikaNatPunchRelayService 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) + { + logger.LogError($"FikaNatPunchServer Websocket error: {e.Message}"); + _webSocket.Close(); + } + + private void WebSocket_OnClose(object sender, CloseEventArgs e) + { + logger.LogInfo($"Disconnected from FikaNatPunchService as server"); + } + + private void ProcessMessage(string data) + { + var msgObj = GetRequestObject(data); + var msgObjType = msgObj.GetType().Name; + + switch (msgObjType) + { + case "GetHostStunRequest": + var getHostStunRequest = (GetHostStunRequest)msgObj; + + if (_stunIpEndPoint != null) + { + IPEndPoint clientIpEndPoint = new IPEndPoint(IPAddress.Parse(getHostStunRequest.StunIp), getHostStunRequest.StunPort); + + NatPunchUtils.PunchNat(_netManager, clientIpEndPoint); + + SendHostStun(getHostStunRequest.SessionId, _stunIpEndPoint); + } + 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); + + 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("Invalid requestType received!"); + } + } + + 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..c7858bf9 --- /dev/null +++ b/Fika.Core/Networking/NatPunch/NatPunchUtils.cs @@ -0,0 +1,109 @@ +using LiteNetLib.Utils; +using LiteNetLib; +using STUN; +using System; +using System.Net.Sockets; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using EFT.UI; +using BepInEx.Logging; + +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 + { + private static ManualLogSource logger = Logger.CreateLogSource("Fika.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; + + IPEndPoint 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) + { + logger.LogError($"Error during STUN query: {ex.Message}"); + } + finally + { + stunUdpClient.Client.Close(); + } + + return null; + } + + public static async void StunQueryRoutine(NetManager netManager, FikaNatPunchServer fikaNatPunchServer, CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + netManager.Stop(); + + int localPort = 0; + + StunIPEndPoint currentStunIpEndPoint = fikaNatPunchServer.StunIPEndpoint; + + if (currentStunIpEndPoint != null) + localPort = currentStunIpEndPoint.Local.Port; + + StunIPEndPoint stunIpEndPoint = CreateStunEndPoint(localPort); + + if(stunIpEndPoint == null) + { + logger.LogError("Error during STUN query routine: Stun Endpoint is null."); + break; + } + + fikaNatPunchServer.StunIPEndpoint = stunIpEndPoint; + + netManager.Start(stunIpEndPoint.Local.Port); + + await Task.Delay(TimeSpan.FromSeconds(60)); + } + } + + public static void PunchNat(NetManager netManager, IPEndPoint endPoint) + { + // bogus punch data + var resp = new NetDataWriter(); + resp.Put("fika.punchnat"); + + // 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 diff --git a/Fika.Core/UI/Custom/MatchMakerUIScript.cs b/Fika.Core/UI/Custom/MatchMakerUIScript.cs index d3d80942..49b770b9 100644 --- a/Fika.Core/UI/Custom/MatchMakerUIScript.cs +++ b/Fika.Core/UI/Custom/MatchMakerUIScript.cs @@ -6,6 +6,7 @@ using Fika.Core.Networking; using Fika.Core.Networking.Http; using Fika.Core.Networking.Http.Models; +using Fika.Core.Networking.NatPunch; using Fika.Core.UI.Models; using HarmonyLib; using System; @@ -209,8 +210,11 @@ private IEnumerator JoinMatch(string profileId, string serverId, Button button) NotificationManagerClass.DisplayMessageNotification("Connecting to session...", iconType: EFT.Communications.ENotificationIconType.EntryPoint); - FikaPingingClient pingingClient = new(serverId); - if (pingingClient.Init()) + NetManagerUtils.CreatePingingClient(); + + var pingingClient = Singleton.Instance; + + if (pingingClient.Init(serverId)) { int attempts = 0; bool success; @@ -221,7 +225,7 @@ private IEnumerator JoinMatch(string profileId, string serverId, Button button) { attempts++; - pingingClient.PingEndPoint(); + pingingClient.PingEndPoint("fika.hello"); pingingClient.NetClient.PollEvents(); success = pingingClient.Received; @@ -249,21 +253,29 @@ private IEnumerator JoinMatch(string profileId, string serverId, Button button) ConsoleScreen.Log("ERROR"); } - pingingClient.NetClient?.Stop(); - pingingClient = null; - if (FikaBackendUtils.JoinMatch(profileId, serverId, out CreateMatch result, out string errorMessage)) { FikaBackendUtils.SetGroupId(result.ServerId); FikaBackendUtils.MatchingType = EMatchmakerType.GroupPlayer; FikaBackendUtils.HostExpectedNumberOfPlayers = result.ExpectedNumberOfPlayers; + if (FikaBackendUtils.IsHostNatPunch) + { + pingingClient.StartKeepAliveRoutine(); + } + else + { + NetManagerUtils.DestroyPingingClient(); + } + DestroyThis(); AcceptButton.OnClick.Invoke(); } else { + NetManagerUtils.DestroyPingingClient(); + Singleton.Instance.ShowCriticalErrorScreen("ERROR JOINING", errorMessage, ErrorScreen.EButtonType.OkButton, 15, null, null); } } diff --git a/References/websocket-sharp.dll b/References/websocket-sharp.dll new file mode 100644 index 00000000..1a677258 Binary files /dev/null and b/References/websocket-sharp.dll differ