From 2732aa90a13e82348acbac884243a25e45ac25bb Mon Sep 17 00:00:00 2001 From: Derek S <44935661+Derek-R-S@users.noreply.github.com> Date: Sat, 3 Apr 2021 21:52:21 -0500 Subject: [PATCH] Client-Side NAT punchtrough code --- UnityTransport/BiDictionary.cs | 5 + UnityTransport/HelpAttribute.cs | 230 ++++++++++ UnityTransport/LRMDirectConnectModule.cs | 159 +++++++ .../LightReflectiveMirrorTransport.cs | 402 +++++++++++++++++- UnityTransport/SocketProxy.cs | 67 +++ 5 files changed, 841 insertions(+), 22 deletions(-) create mode 100644 UnityTransport/HelpAttribute.cs create mode 100644 UnityTransport/LRMDirectConnectModule.cs create mode 100644 UnityTransport/SocketProxy.cs diff --git a/UnityTransport/BiDictionary.cs b/UnityTransport/BiDictionary.cs index 1bdd4b8..507c85f 100644 --- a/UnityTransport/BiDictionary.cs +++ b/UnityTransport/BiDictionary.cs @@ -33,6 +33,11 @@ namespace LightReflectiveMirror firstToSecond.Remove(first); } + public ICollection GetAllKeys() + { + return secondToFirst.Values; + } + public bool TryGetBySecond(TSecond second, out TFirst first) { return secondToFirst.TryGetValue(second, out first); diff --git a/UnityTransport/HelpAttribute.cs b/UnityTransport/HelpAttribute.cs new file mode 100644 index 0000000..e4a08d0 --- /dev/null +++ b/UnityTransport/HelpAttribute.cs @@ -0,0 +1,230 @@ +// -------------------------------------------------------------------------------------------------------------------- +/// +/// +/// Copyright (c) 2017, John Earnshaw, reblGreen Software Limited +/// +/// +/// +/// All rights reserved. +/// Redistribution and use in source and binary forms, with or without modification, are +/// permitted provided that the following conditions are met: +/// 1. Redistributions of source code must retain the above copyright notice, this list of +/// conditions and the following disclaimer. +/// 2. Redistributions in binary form must reproduce the above copyright notice, this list +/// of conditions and the following disclaimer in the documentation and/or other materials +/// provided with the distribution. +/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +/// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +/// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT SHALL THE +/// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +/// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +/// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +/// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +/// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +/// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +/// +// -------------------------------------------------------------------------------------------------------------------- +using System; +using UnityEngine; +#if UNITY_EDITOR +using UnityEditor; +#endif + +[AttributeUsage(AttributeTargets.Field, Inherited = true)] +public class HelpAttribute : PropertyAttribute +{ + public readonly string text; + + // MessageType exists in UnityEditor namespace and can throw an exception when used outside the editor. + // We spoof MessageType at the bottom of this script to ensure that errors are not thrown when + // MessageType is unavailable. + public readonly MessageType type; + + + /// + /// Adds a HelpBox to the Unity property inspector above this field. + /// + /// The help text to be displayed in the HelpBox. + /// The icon to be displayed in the HelpBox. + public HelpAttribute(string text, MessageType type = MessageType.Info) + { + this.text = text; + this.type = type; + } +} + +#if UNITY_EDITOR +[CustomPropertyDrawer(typeof(HelpAttribute))] +public class HelpDrawer : PropertyDrawer +{ + // Used for top and bottom padding between the text and the HelpBox border. + const int paddingHeight = 8; + + // Used to add some margin between the the HelpBox and the property. + const int marginHeight = 2; + + // Global field to store the original (base) property height. + float baseHeight = 0; + + // Custom added height for drawing text area which has the MultilineAttribute. + float addedHeight = 0; + + /// + /// A wrapper which returns the PropertyDrawer.attribute field as a HelpAttribute. + /// + HelpAttribute helpAttribute { get { return (HelpAttribute)attribute; } } + + /// + /// A helper property to check for RangeAttribute. + /// + RangeAttribute rangeAttribute + { + get + { + var attributes = fieldInfo.GetCustomAttributes(typeof(RangeAttribute), true); + return attributes != null && attributes.Length > 0 ? (RangeAttribute)attributes[0] : null; + } + } + + /// + /// A helper property to check for MultiLineAttribute. + /// + MultilineAttribute multilineAttribute + { + get + { + var attributes = fieldInfo.GetCustomAttributes(typeof(MultilineAttribute), true); + return attributes != null && attributes.Length > 0 ? (MultilineAttribute)attributes[0] : null; + } + } + + + public override float GetPropertyHeight(SerializedProperty prop, GUIContent label) + { + // We store the original property height for later use... + baseHeight = base.GetPropertyHeight(prop, label); + + // This stops icon shrinking if text content doesn't fill out the container enough. + float minHeight = paddingHeight * 5; + + // Calculate the height of the HelpBox using the GUIStyle on the current skin and the inspector + // window's currentViewWidth. + var content = new GUIContent(helpAttribute.text); + var style = GUI.skin.GetStyle("helpbox"); + + var height = style.CalcHeight(content, EditorGUIUtility.currentViewWidth); + + // We add tiny padding here to make sure the text is not overflowing the HelpBox from the top + // and bottom. + height += marginHeight * 2; + + // Since we draw a custom text area with the label above if our property contains the + // MultilineAttribute, we need to add some extra height to compensate. This is stored in a + // seperate global field so we can use it again later. + if (multilineAttribute != null && prop.propertyType == SerializedPropertyType.String) + { + addedHeight = 48f; + } + + // If the calculated HelpBox is less than our minimum height we use this to calculate the returned + // height instead. + return height > minHeight ? height + baseHeight + addedHeight : minHeight + baseHeight + addedHeight; + } + + + public override void OnGUI(Rect position, SerializedProperty prop, GUIContent label) + { + // We get a local reference to the MultilineAttribute as we use it twice in this method and it + // saves calling the logic twice for minimal optimization, etc... + var multiline = multilineAttribute; + + EditorGUI.BeginProperty(position, label, prop); + + // Copy the position out so we can calculate the position of our HelpBox without affecting the + // original position. + var helpPos = position; + + helpPos.height -= baseHeight + marginHeight; + + + if (multiline != null) + { + helpPos.height -= addedHeight; + } + + // Renders the HelpBox in the Unity inspector UI. + EditorGUI.HelpBox(helpPos, helpAttribute.text, helpAttribute.type); + + position.y += helpPos.height + marginHeight; + position.height = baseHeight; + + + // If we have a RangeAttribute on our field, we need to handle the PropertyDrawer differently to + // keep the same style as Unity's default. + var range = rangeAttribute; + + if (range != null) + { + if (prop.propertyType == SerializedPropertyType.Float) + { + EditorGUI.Slider(position, prop, range.min, range.max, label); + } + else if (prop.propertyType == SerializedPropertyType.Integer) + { + EditorGUI.IntSlider(position, prop, (int)range.min, (int)range.max, label); + } + else + { + // Not numeric so draw standard property field as punishment for adding RangeAttribute to + // a property which can not have a range :P + EditorGUI.PropertyField(position, prop, label); + } + } + else if (multiline != null) + { + // Here's where we handle the PropertyDrawer differently if we have a MultiLineAttribute, to try + // and keep some kind of multiline text area. This is not identical to Unity's default but is + // better than nothing... + if (prop.propertyType == SerializedPropertyType.String) + { + var style = GUI.skin.label; + var size = style.CalcHeight(label, EditorGUIUtility.currentViewWidth); + + EditorGUI.LabelField(position, label); + + position.y += size; + position.height += addedHeight - size; + + // Fixed text dissappearing thanks to: http://answers.unity3d.com/questions/244043/textarea-does-not-work-text-dissapears-solution-is.html + prop.stringValue = EditorGUI.TextArea(position, prop.stringValue); + } + else + { + // Again with a MultilineAttribute on a non-text field deserves for the standard property field + // to be drawn as punishment :P + EditorGUI.PropertyField(position, prop, label); + } + } + else + { + // If we get to here it means we're drawing the default property field below the HelpBox. More custom + // and built in PropertyDrawers could be implemented to enable HelpBox but it could easily make for + // hefty else/if block which would need refactoring! + EditorGUI.PropertyField(position, prop, label); + } + + EditorGUI.EndProperty(); + } +} +#else + // Replicate MessageType Enum if we are not in editor as this enum exists in UnityEditor namespace. + // This should stop errors being logged the same as Shawn Featherly's commit in the Github repo but I + // feel is cleaner than having the conditional directive in the middle of the HelpAttribute constructor. + public enum MessageType + { + None, + Info, + Warning, + Error, + } +#endif diff --git a/UnityTransport/LRMDirectConnectModule.cs b/UnityTransport/LRMDirectConnectModule.cs new file mode 100644 index 0000000..155497d --- /dev/null +++ b/UnityTransport/LRMDirectConnectModule.cs @@ -0,0 +1,159 @@ +// This is an optional module for adding direct connect support + +using Mirror; +using System; +using System.Collections.Generic; +using UnityEngine; +using LightReflectiveMirror; + +[RequireComponent(typeof(LightReflectiveMirrorTransport))] +public class LRMDirectConnectModule : MonoBehaviour +{ + public Transport directConnectTransport; + public bool showDebugLogs; + private LightReflectiveMirrorTransport lightMirrorTransport; + + void Awake() + { + lightMirrorTransport = GetComponent(); + + if (directConnectTransport == null) + { + Debug.Log("Direct Connect Transport is null!"); + return; + } + + if (directConnectTransport is LightReflectiveMirrorTransport) + { + Debug.Log("Direct Connect Transport Cannot be the relay, silly. :P"); + return; + } + + directConnectTransport.OnServerConnected = (OnServerConnected); + directConnectTransport.OnServerDataReceived = (OnServerDataReceived); + directConnectTransport.OnServerDisconnected = (OnServerDisconnected); + directConnectTransport.OnServerError = (OnServerError); + directConnectTransport.OnClientConnected = (OnClientConnected); + directConnectTransport.OnClientDataReceived = (OnClientDataReceived); + directConnectTransport.OnClientDisconnected = (OnClientDisconnected); + directConnectTransport.OnClientError = (OnClientError); + } + + public void StartServer(int port) + { + if(port > 0) + SetTransportPort(port); + + directConnectTransport.ServerStart(); + if (showDebugLogs) + Debug.Log("Direct Connect Server Created!"); + } + + public void StopServer() + { + directConnectTransport.ServerStop(); + } + + public void JoinServer(string ip, int port) + { + if(SupportsNATPunch()) + SetTransportPort(port); + directConnectTransport.ClientConnect(ip); + } + + public void SetTransportPort(int port) + { + if (directConnectTransport is kcp2k.KcpTransport kcpTransport) + kcpTransport.Port = (ushort)port; + else + { + throw new Exception("DIRECT CONNECT MODULE ONLY SUPPORTS KCP AT THE MOMENT."); + } + } + + public int GetTransportPort() + { + if (directConnectTransport is kcp2k.KcpTransport kcpTransport) + return kcpTransport.Port; + else + { + throw new Exception("DIRECT CONNECT MODULE ONLY SUPPORTS KCP AT THE MOMENT."); + } + } + + public bool SupportsNATPunch() + { + return directConnectTransport is kcp2k.KcpTransport; + } + + public bool KickClient(int clientID) + { + if (showDebugLogs) + Debug.Log("Kicked direct connect client."); + return directConnectTransport.ServerDisconnect(clientID); + } + + public void ClientDisconnect() + { + directConnectTransport.ClientDisconnect(); + } + + public void ServerSend(int clientID, ArraySegment data, int channel) + { + directConnectTransport.ServerSend(clientID, channel, data); + } + + public void ClientSend(ArraySegment data, int channel) + { + directConnectTransport.ClientSend(channel, data); + } + + #region Transport Callbacks + void OnServerConnected(int clientID) + { + if (showDebugLogs) + Debug.Log("Direct Connect Client Connected"); + lightMirrorTransport.DirectAddClient(clientID); + } + + void OnServerDataReceived(int clientID, ArraySegment data, int channel) + { + lightMirrorTransport.DirectReceiveData(data, channel, clientID); + } + + void OnServerDisconnected(int clientID) + { + lightMirrorTransport.DirectRemoveClient(clientID); + } + + void OnServerError(int client, Exception error) + { + if (showDebugLogs) + Debug.Log("Direct Server Error: " + error); + } + + void OnClientConnected() + { + if (showDebugLogs) + Debug.Log("Direct Connect Client Joined"); + + lightMirrorTransport.DirectClientConnected(); + } + + void OnClientDisconnected() + { + lightMirrorTransport.DirectDisconnected(); + } + + void OnClientDataReceived(ArraySegment data, int channel) + { + lightMirrorTransport.DirectReceiveData(data, channel); + } + + void OnClientError(Exception error) + { + if (showDebugLogs) + Debug.Log("Direct Client Error: " + error); + } + #endregion +} \ No newline at end of file diff --git a/UnityTransport/LightReflectiveMirrorTransport.cs b/UnityTransport/LightReflectiveMirrorTransport.cs index ec02026..c539e79 100644 --- a/UnityTransport/LightReflectiveMirrorTransport.cs +++ b/UnityTransport/LightReflectiveMirrorTransport.cs @@ -4,6 +4,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Net.Sockets; using UnityEngine; using UnityEngine.Events; using UnityEngine.Networking; @@ -21,6 +23,10 @@ namespace LightReflectiveMirror public bool connectOnAwake = true; public string authenticationKey = "Secret Auth Key"; public UnityEvent diconnectedFromRelay; + [Header("NAT Punchthrough")] + [Help("NAT Punchthrough will require the Direct Connect module attached.")] + public bool useNATPunch = true; + public ushort NATPunchtroughPort = 7776; [Header("Server Hosting Data")] public string serverName = "My awesome server!"; public string extraServerData = "Map 1"; @@ -32,31 +38,137 @@ namespace LightReflectiveMirror [Header("Server Information")] public int serverId = -1; + private LRMDirectConnectModule _directConnectModule; + private byte[] _clientSendBuffer; private bool _connectedToRelay = false; private bool _isClient = false; private bool _isServer = false; + private bool _directConnected = false; private bool _isAuthenticated = false; private int _currentMemberId; private bool _callbacksInitialized = false; + private int _cachedHostID; private BiDictionary _connectedRelayClients = new BiDictionary(); + private BiDictionary _connectedDirectClients = new BiDictionary(); + private UdpClient _NATPuncher; + private IPEndPoint _NATIP; + private IPEndPoint _relayPuncherIP; + private byte[] _punchData = new byte[1] { 1 }; + private IPEndPoint _directConnectEndpoint; + private SocketProxy _clientProxy; + private BiDictionary _serverProxies = new BiDictionary(); public override bool ClientConnected() => _isClient; private void OnConnectedToRelay() => _connectedToRelay = true; public bool IsAuthenticated() => _isAuthenticated; public override bool ServerActive() => _isServer; public override bool Available() => _connectedToRelay; - public override void ClientEarlyUpdate() => clientToServerTransport.ClientEarlyUpdate(); - public override void ClientLateUpdate() => clientToServerTransport.ClientLateUpdate(); public override void ClientConnect(Uri uri) => ClientConnect(uri.Host); public override int GetMaxPacketSize(int channelId = 0) => clientToServerTransport.GetMaxPacketSize(channelId); - public override string ServerGetClientAddress(int connectionId) => _connectedRelayClients.GetBySecond(connectionId).ToString(); + public override string ServerGetClientAddress(int connectionId) { + if (_connectedRelayClients.TryGetBySecond(connectionId, out int relayId)) + return relayId.ToString(); + + if (_connectedDirectClients.TryGetBySecond(connectionId, out int directId)) + return "DIRECT-" + directId; + + // Shouldn't ever get here. + return "?"; + } + + public override void ClientEarlyUpdate() + { + clientToServerTransport.ClientEarlyUpdate(); + + if (_directConnectModule != null) + _directConnectModule.directConnectTransport.ClientEarlyUpdate(); + } + + public override void ClientLateUpdate() + { + clientToServerTransport.ClientLateUpdate(); + + if (_directConnectModule != null) + _directConnectModule.directConnectTransport.ClientLateUpdate(); + } + + public override void ServerEarlyUpdate() + { + if (_directConnectModule != null) + _directConnectModule.directConnectTransport.ServerEarlyUpdate(); + } + + void RecvData(IAsyncResult result) + { + IPEndPoint newClientEP = new IPEndPoint(IPAddress.Any, 0); + var data = _NATPuncher.EndReceive(result, ref newClientEP); + + if (!newClientEP.Address.Equals(_relayPuncherIP.Address)) + { + if (_isServer) + { + if(_serverProxies.TryGetByFirst(newClientEP, out SocketProxy foundProxy)) + { + if (data.Length > 2) + foundProxy.RelayData(data, data.Length); + } + else + { + _serverProxies.Add(newClientEP, new SocketProxy(_NATIP.Port + 1, newClientEP)); + _serverProxies.GetByFirst(newClientEP).dataReceived += ServerProcessProxyData; + } + } + + if (_isClient) + { + if(_clientProxy == null) + { + _clientProxy = new SocketProxy(_NATIP.Port - 1); + _clientProxy.dataReceived += ClientProcessProxyData; + } + else + { + _clientProxy.ClientRelayData(data, data.Length); + } + } + } + + _NATPuncher.BeginReceive(new AsyncCallback(RecvData), _NATPuncher); + } + + void ServerProcessProxyData(IPEndPoint remoteEndpoint, byte[] data) + { + _NATPuncher.Send(data, data.Length, remoteEndpoint); + } + + void ClientProcessProxyData(IPEndPoint _, byte[] data) + { + _NATPuncher.Send(data, data.Length, _directConnectEndpoint); + } + + public override void ServerLateUpdate() + { + if (_directConnectModule != null) + _directConnectModule.directConnectTransport.ServerLateUpdate(); + } private void Awake() { if (clientToServerTransport is LightReflectiveMirrorTransport) throw new Exception("Haha real funny... Use a different transport."); + _directConnectModule = GetComponent(); + + if(_directConnectModule != null) + { + if (useNATPunch && !_directConnectModule.SupportsNATPunch()) + { + Debug.LogWarning("LRM | NATPunch is turned on but the transport used does not support it. It will be disabled."); + useNATPunch = false; + } + } + SetupCallbacks(); if (connectOnAwake) @@ -104,6 +216,20 @@ namespace LightReflectiveMirror int pos = 0; _clientSendBuffer.WriteByte(ref pos, 200); clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); + + if (_NATPuncher != null) + _NATPuncher.Send(new byte[] { 0 }, 1, _relayPuncherIP); + + var keys = new List(_serverProxies.GetAllKeys()); + + for (int i = 0; i < keys.Count; i++) + { + if (DateTime.Now.Subtract(_serverProxies.GetByFirst(keys[i]).lastInteractionTime).TotalSeconds > 10) + { + _serverProxies.GetByFirst(keys[i]).Dispose(); + _serverProxies.Remove(keys[i]); + } + } } } @@ -115,6 +241,15 @@ namespace LightReflectiveMirror Debug.Log("You must be connected to Relay to request server list!"); } + IEnumerator NATPunch(IPEndPoint remoteAddress) + { + for (int i = 0; i < 10; i++) + { + _NATPuncher.Send(_punchData, 1, remoteAddress); + yield return new WaitForSeconds(0.25f); + } + } + void DataReceived(ArraySegment segmentData, int channel) { try @@ -136,7 +271,10 @@ namespace LightReflectiveMirror var recvData = data.ReadBytes(ref pos); if (_isServer) - OnServerDataReceived?.Invoke(_connectedRelayClients.GetByFirst(data.ReadInt(ref pos)), new ArraySegment(recvData), channel); + { + if(_connectedRelayClients.TryGetByFirst(data.ReadInt(ref pos), out int clientID)) + OnServerDataReceived?.Invoke(clientID, new ArraySegment(recvData), channel); + } if (_isClient) OnClientDataReceived?.Invoke(new ArraySegment(recvData), channel); @@ -152,8 +290,11 @@ namespace LightReflectiveMirror if (_isServer) { int user = data.ReadInt(ref pos); - OnServerDisconnected?.Invoke(_connectedRelayClients.GetByFirst(user)); - _connectedRelayClients.Remove(user); + if (_connectedRelayClients.TryGetByFirst(user, out int clientID)) + { + OnServerDisconnected?.Invoke(_connectedRelayClients.GetByFirst(clientID)); + _connectedRelayClients.Remove(user); + } } break; case OpCodes.RoomCreated: @@ -172,14 +313,64 @@ namespace LightReflectiveMirror _currentMemberId++; } break; + case OpCodes.DirectConnectIP: + var ip = data.ReadString(ref pos); + int port = data.ReadInt(ref pos); + bool attemptNatPunch = data.ReadBool(ref pos); + + _directConnectEndpoint = new IPEndPoint(IPAddress.Parse(ip), port); + + if (useNATPunch) + { + StartCoroutine(NATPunch(_directConnectEndpoint)); + } + + if (!_isServer) + { + if (_clientProxy == null && useNATPunch && attemptNatPunch) + { + _clientProxy = new SocketProxy(_NATIP.Port - 1); + _clientProxy.dataReceived += ClientProcessProxyData; + } + + if (useNATPunch && attemptNatPunch) + _directConnectModule.JoinServer("127.0.0.1", _NATIP.Port - 1); + else + _directConnectModule.JoinServer(ip, port); + } + + break; + case OpCodes.RequestNATConnection: + if (GetLocalIp() != null && _directConnectModule != null) + { + _NATPuncher = new UdpClient(); + _NATPuncher.ExclusiveAddressUse = false; + _NATIP = new IPEndPoint(IPAddress.Parse(GetLocalIp()), UnityEngine.Random.Range(16000, 17000)); + _NATPuncher.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _NATPuncher.Client.Bind(_NATIP); + _relayPuncherIP = new IPEndPoint(IPAddress.Parse(serverIP), NATPunchtroughPort); + + byte[] initalData = new byte[150]; + int sendPos = 0; + + initalData.WriteBool(ref sendPos, true); + initalData.WriteString(ref sendPos, data.ReadString(ref pos)); + + // Send 3 to lower chance of it being dropped or corrupted when received on server. + _NATPuncher.Send(initalData, sendPos,_relayPuncherIP); + _NATPuncher.Send(initalData, sendPos,_relayPuncherIP); + _NATPuncher.Send(initalData, sendPos, _relayPuncherIP); + _NATPuncher.BeginReceive(new AsyncCallback(RecvData), _NATPuncher); + } + break; } } - catch { } + catch(Exception e) { print(e); } } IEnumerator GetServerList() { - Uri uri = new Uri($"http://{serverIP}:{endpointServerPort}/api/servers"); + string uri = $"http://{serverIP}:{endpointServerPort}/api/servers"; using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) { @@ -187,6 +378,7 @@ namespace LightReflectiveMirror yield return webRequest.SendWebRequest(); var result = webRequest.downloadHandler.text; +#if UNITY_2020_1_OR_NEWER switch (webRequest.result) { case UnityWebRequest.Result.ConnectionError: @@ -209,6 +401,25 @@ namespace LightReflectiveMirror break; } } +#else + if (webRequest.isNetworkError || webRequest.isHttpError) + { + Debug.LogWarning("LRM | Network Error while retreiving the server list!"); + } + else + { + if (result == "Access Denied") + { + Debug.LogWarning("LRM | Server list request denied. Make sure you enable 'EndpointServerList' in server config!"); + } + else + { + relayServerList?.Clear(); + relayServerList = JsonConvert.DeserializeObject>(result); + serverListUpdated?.Invoke(); + } + } +#endif } } @@ -266,8 +477,7 @@ namespace LightReflectiveMirror public override void ClientConnect(string address) { - int hostId = 0; - if (!Available() || !int.TryParse(address, out hostId)) + if (!Available() || !int.TryParse(address, out _cachedHostID)) { Debug.Log("Not connected to relay or invalid server id!"); OnClientDisconnected?.Invoke(); @@ -278,8 +488,15 @@ namespace LightReflectiveMirror throw new Exception("Cannot connect while hosting/already connected!"); int pos = 0; + _directConnected = false; _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.JoinServer); - _clientSendBuffer.WriteInt(ref pos, hostId); + _clientSendBuffer.WriteInt(ref pos, _cachedHostID); + _clientSendBuffer.WriteBool(ref pos, _directConnectModule != null); + + if (GetLocalIp() == null) + _clientSendBuffer.WriteString(ref pos, "0.0.0.0"); + else + _clientSendBuffer.WriteString(ref pos, GetLocalIp()); _isClient = true; @@ -294,16 +511,26 @@ namespace LightReflectiveMirror _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.LeaveRoom); clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); + + if (_directConnectModule != null) + _directConnectModule.ClientDisconnect(); } public override void ClientSend(int channelId, ArraySegment segment) { - int pos = 0; - _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.SendData); - _clientSendBuffer.WriteBytes(ref pos, segment.Array.Take(segment.Count).ToArray()); - _clientSendBuffer.WriteInt(ref pos, 0); + if (_directConnected) + { + _directConnectModule.ClientSend(segment, channelId); + } + else + { + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.SendData); + _clientSendBuffer.WriteBytes(ref pos, segment.Array.Take(segment.Count).ToArray()); + _clientSendBuffer.WriteInt(ref pos, 0); - clientToServerTransport.ClientSend(channelId, new ArraySegment(_clientSendBuffer, 0, pos)); + clientToServerTransport.ClientSend(channelId, new ArraySegment(_clientSendBuffer, 0, pos)); + } } public override bool ServerDisconnect(int connectionId) @@ -316,17 +543,27 @@ namespace LightReflectiveMirror return true; } + if(_connectedDirectClients.TryGetBySecond(connectionId, out int directId)) + return _directConnectModule.KickClient(directId); + return false; } public override void ServerSend(int connectionId, int channelId, ArraySegment segment) { - int pos = 0; - _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.SendData); - _clientSendBuffer.WriteBytes(ref pos, segment.Array.Take(segment.Count).ToArray()); - _clientSendBuffer.WriteInt(ref pos, _connectedRelayClients.GetBySecond(connectionId)); + if (_directConnectModule != null && _connectedDirectClients.TryGetBySecond(connectionId, out int directId)) + { + _directConnectModule.ServerSend(directId, segment, channelId); + } + else + { + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.SendData); + _clientSendBuffer.WriteBytes(ref pos, segment.Array.Take(segment.Count).ToArray()); + _clientSendBuffer.WriteInt(ref pos, _connectedRelayClients.GetBySecond(connectionId)); - clientToServerTransport.ClientSend(channelId, new ArraySegment(_clientSendBuffer, 0, pos)); + clientToServerTransport.ClientSend(channelId, new ArraySegment(_clientSendBuffer, 0, pos)); + } } public override void ServerStart() @@ -346,6 +583,15 @@ namespace LightReflectiveMirror _isServer = true; _connectedRelayClients = new BiDictionary(); _currentMemberId = 1; + _connectedDirectClients = new BiDictionary(); + + var keys = new List(_serverProxies.GetAllKeys()); + + for(int i = 0; i < keys.Count; i++) + { + _serverProxies.GetByFirst(keys[i]).Dispose(); + _serverProxies.Remove(keys[i]); + } int pos = 0; _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.CreateRoom); @@ -353,6 +599,28 @@ namespace LightReflectiveMirror _clientSendBuffer.WriteString(ref pos, serverName); _clientSendBuffer.WriteBool(ref pos, isPublicServer); _clientSendBuffer.WriteString(ref pos, extraServerData); + // If we have direct connect module, and our local IP isnt null, tell server. Only time local IP is null is on cellular networks, such as IOS and Android. + _clientSendBuffer.WriteBool(ref pos, _directConnectModule != null ? GetLocalIp() != null ? true : false : false); + + if (_directConnectModule != null && GetLocalIp() != null) + { + _clientSendBuffer.WriteString(ref pos, GetLocalIp()); + // Transport port will be NAT port + 1 for the proxy connections. + _directConnectModule.StartServer(useNATPunch ? _NATIP.Port + 1 : -1); + } + else + _clientSendBuffer.WriteString(ref pos, "0.0.0.0"); + + if (useNATPunch) + { + _clientSendBuffer.WriteBool(ref pos, true); + _clientSendBuffer.WriteInt(ref pos, 0); + } + else + { + _clientSendBuffer.WriteBool(ref pos, false); + _clientSendBuffer.WriteInt(ref pos, _directConnectModule == null ? 1 : _directConnectModule.SupportsNATPunch() ? _directConnectModule.GetTransportPort() : 1); + } clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); } @@ -366,6 +634,17 @@ namespace LightReflectiveMirror _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.LeaveRoom); clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); + + if (_directConnectModule != null) + _directConnectModule.StopServer(); + + var keys = new List(_serverProxies.GetAllKeys()); + + for (int i = 0; i < keys.Count; i++) + { + _serverProxies.GetByFirst(keys[i]).Dispose(); + _serverProxies.Remove(keys[i]); + } } } @@ -392,8 +671,87 @@ namespace LightReflectiveMirror public enum OpCodes { Default = 0, RequestID = 1, JoinServer = 2, SendData = 3, GetID = 4, ServerJoined = 5, GetData = 6, CreateRoom = 7, ServerLeft = 8, PlayerDisconnected = 9, RoomCreated = 10, - LeaveRoom = 11, KickPlayer = 12, AuthenticationRequest = 13, AuthenticationResponse = 14, RequestServers = 15, ServerListReponse = 16, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19 + LeaveRoom = 11, KickPlayer = 12, AuthenticationRequest = 13, AuthenticationResponse = 14, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19, RequestNATConnection = 20, + DirectConnectIP = 21 } + + private static string GetLocalIp() + { + var host = Dns.GetHostEntry(Dns.GetHostName()); + foreach (var ip in host.AddressList) + { + if (ip.AddressFamily == AddressFamily.InterNetwork) + { + return ip.ToString(); + } + } + + return null; + } + + #region Direct Connect Module + public void DirectAddClient(int clientID) + { + if (!_isServer) + return; + + _connectedDirectClients.Add(clientID, _currentMemberId); + OnServerConnected?.Invoke(_currentMemberId); + _currentMemberId++; + } + + public void DirectRemoveClient(int clientID) + { + if (!_isServer) + return; + + OnServerDisconnected?.Invoke(_connectedDirectClients.GetByFirst(clientID)); + _connectedDirectClients.Remove(clientID); + } + + public void DirectReceiveData(ArraySegment data, int channel, int clientID = -1) + { + if (_isServer) + OnServerDataReceived?.Invoke(_connectedDirectClients.GetByFirst(clientID), data, channel); + + if (_isClient) + OnClientDataReceived?.Invoke(data, channel); + } + + public void DirectClientConnected() + { + _directConnected = true; + OnClientConnected?.Invoke(); + } + + public void DirectDisconnected() + { + if (_directConnected) + { + _isClient = false; + _directConnected = false; + OnClientDisconnected?.Invoke(); + } + else + { + int pos = 0; + _directConnected = false; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.JoinServer); + _clientSendBuffer.WriteInt(ref pos, _cachedHostID); + _clientSendBuffer.WriteBool(ref pos, false); // Direct failed, use relay + + _isClient = true; + + clientToServerTransport.ClientSend(0, new System.ArraySegment(_clientSendBuffer, 0, pos)); + } + + if (_clientProxy != null) + { + _clientProxy.Dispose(); + _clientProxy = null; + } + } + #endregion } [Serializable] diff --git a/UnityTransport/SocketProxy.cs b/UnityTransport/SocketProxy.cs new file mode 100644 index 0000000..fa611e7 --- /dev/null +++ b/UnityTransport/SocketProxy.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using UnityEngine; + +namespace LightReflectiveMirror +{ + + // This class handles the proxying from punched socket to transport. + public class SocketProxy + { + public DateTime lastInteractionTime; + public Action dataReceived; + UdpClient _udpClient; + IPEndPoint _recvEndpoint = new IPEndPoint(IPAddress.Any, 0); + IPEndPoint _remoteEndpoint; + bool _clientInitialRecv = false; + + public SocketProxy(int port, IPEndPoint remoteEndpoint) + { + _udpClient = new UdpClient(); + _udpClient.Connect(new IPEndPoint(IPAddress.Loopback, port)); + _udpClient.BeginReceive(new AsyncCallback(RecvData), _udpClient); + lastInteractionTime = DateTime.Now; + // Clone it so when main socket recvies new data, it wont switcheroo on us. + _remoteEndpoint = new IPEndPoint(remoteEndpoint.Address, remoteEndpoint.Port); + } + + public SocketProxy(int port) + { + _udpClient = new UdpClient(port); + _udpClient.BeginReceive(new AsyncCallback(RecvData), _udpClient); + lastInteractionTime = DateTime.Now; + } + + public void RelayData(byte[] data, int length) + { + _udpClient.Send(data, length); + lastInteractionTime = DateTime.Now; + } + + public void ClientRelayData(byte[] data, int length) + { + if (_clientInitialRecv) + { + _udpClient.Send(data, length, _recvEndpoint); + lastInteractionTime = DateTime.Now; + } + } + + public void Dispose() + { + _udpClient.Dispose(); + } + + void RecvData(IAsyncResult result) + { + _clientInitialRecv = true; + byte[] data = _udpClient.EndReceive(result, ref _recvEndpoint); + lastInteractionTime = DateTime.Now; + dataReceived?.Invoke(_remoteEndpoint, data); + _udpClient.BeginReceive(new AsyncCallback(RecvData), _udpClient); + } + } +} \ No newline at end of file