diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index b817a0b6ea..3069602122 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,6 +10,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added +- When using UnityTransport >=2.4 and Unity >= 6000.1.0a1, SetConnectionData will accept a fully qualified hostname instead of an IP as a connect address on the client side. (#3441) ### Fixed diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs index 210d7f660b..31b20b6630 100644 --- a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs +++ b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs @@ -7,6 +7,9 @@ using System; using System.Collections.Generic; +#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE +using System.Text.RegularExpressions; +#endif using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; @@ -249,26 +252,41 @@ public struct ConnectionAddressData [SerializeField] public string ServerListenAddress; - private static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port, bool silent = false) + private static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port) { NetworkEndpoint endpoint = default; - - if (!NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv4) && - !NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv6)) + if (!NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv4)) { - if (!silent) - { - Debug.LogError($"Invalid network endpoint: {ip}:{port}."); - } + NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv6); } - return endpoint; } + private void InvalidEndpointError() + { + Debug.LogError($"Invalid network endpoint: {Address}:{Port}."); + } + /// /// Endpoint (IP address and port) clients will connect to. /// - public NetworkEndpoint ServerEndPoint => ParseNetworkEndpoint(Address, Port); + public NetworkEndpoint ServerEndPoint + { + get + { + var networkEndpoint = ParseNetworkEndpoint(Address, Port); + if (networkEndpoint == default) + { +#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE + if (!IsValidFqdn(Address)) +#endif + { + InvalidEndpointError(); + } + } + return networkEndpoint; + } + } /// /// Endpoint (IP address and port) server will listen/bind on. @@ -277,30 +295,35 @@ public NetworkEndpoint ListenEndPoint { get { + NetworkEndpoint endpoint = default; if (string.IsNullOrEmpty(ServerListenAddress)) { - var ep = NetworkEndpoint.LoopbackIpv4; + endpoint = NetworkEndpoint.LoopbackIpv4; // If an address was entered and it's IPv6, switch to using ::1 as the // default listen address. (Otherwise we always assume IPv4.) if (!string.IsNullOrEmpty(Address) && ServerEndPoint.Family == NetworkFamily.Ipv6) { - ep = NetworkEndpoint.LoopbackIpv6; + endpoint = NetworkEndpoint.LoopbackIpv6; } - - return ep.WithPort(Port); + endpoint = endpoint.WithPort(Port); } else { - return ParseNetworkEndpoint(ServerListenAddress, Port); + endpoint = ParseNetworkEndpoint(ServerListenAddress, Port); + if (endpoint == default) + { + InvalidEndpointError(); + } } + return endpoint; } } /// /// Returns true if the end point address is of type . /// - public bool IsIpv6 => !string.IsNullOrEmpty(Address) && ParseNetworkEndpoint(Address, Port, true).Family == NetworkFamily.Ipv6; + public bool IsIpv6 => !string.IsNullOrEmpty(Address) && NetworkEndpoint.TryParse(Address, Port, out NetworkEndpoint _, NetworkFamily.Ipv6); } @@ -486,6 +509,15 @@ private NetworkPipeline SelectSendPipeline(NetworkDelivery delivery) return NetworkPipeline.Null; } } +#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE + private static bool IsValidFqdn(string fqdn) + { + // Regular expression to validate FQDN + string pattern = @"^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(? /// Sets IP and Port information. This will be ignored if using the Unity Relay and you should call /// - /// The remote IP address (despite the name, can be an IPv6 address) + /// The remote IP address (despite the name, can be an IPv6 address or a domain name) /// The remote port /// The local listen address public void SetConnectionData(string ipv4Address, ushort port, string listenAddress = null) diff --git a/com.unity.netcode.gameobjects/Runtime/com.unity.netcode.runtime.asmdef b/com.unity.netcode.gameobjects/Runtime/com.unity.netcode.runtime.asmdef index d68a562768..e4de012706 100644 --- a/com.unity.netcode.gameobjects/Runtime/com.unity.netcode.runtime.asmdef +++ b/com.unity.netcode.gameobjects/Runtime/com.unity.netcode.runtime.asmdef @@ -67,6 +67,16 @@ "name": "Unity", "expression": "6000.0.11f1", "define": "COM_UNITY_MODULES_PHYSICS2D_LINEAR" + }, + { + "name": "com.unity.transport", + "expression": "2.4.0", + "define": "UTP_TRANSPORT_2_4_ABOVE" + }, + { + "name": "Unity", + "expression": "6000.1.0a1", + "define": "HOSTNAME_RESOLUTION_AVAILABLE" } ], "noEngineReferences": false diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Transports/UnityTransportTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/Transports/UnityTransportTests.cs index bd9d9e937e..6762fc954d 100644 --- a/com.unity.netcode.gameobjects/Tests/Editor/Transports/UnityTransportTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Editor/Transports/UnityTransportTests.cs @@ -130,10 +130,13 @@ public void UnityTransport_RestartSucceedsAfterFailure() transport.SetConnectionData("127.0.0.", 4242, "127.0.0."); Assert.False(transport.StartServer()); - LogAssert.Expect(LogType.Error, "Invalid network endpoint: 127.0.0.:4242."); - LogAssert.Expect(LogType.Error, "Network listen address (127.0.0.) is Invalid!"); +#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE + LogAssert.Expect(LogType.Error, "Listen network address (127.0.0.) is not a valid Ipv4 or Ipv6 address!"); +#else + LogAssert.Expect(LogType.Error, "Network listen address (127.0.0.) is Invalid!"); +#endif transport.SetConnectionData("127.0.0.1", 4242, "127.0.0.1"); Assert.True(transport.StartServer()); @@ -162,10 +165,12 @@ public void UnityTransport_StartClientFailsWithBadAddress() transport.SetConnectionData("foobar", 4242); Assert.False(transport.StartClient()); - LogAssert.Expect(LogType.Error, "Invalid network endpoint: foobar:4242."); +#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE + LogAssert.Expect(LogType.Error, "Target server network address (foobar) is not a valid Fully Qualified Domain Name!"); +#else LogAssert.Expect(LogType.Error, "Target server network address (foobar) is Invalid!"); - +#endif transport.Shutdown(); } diff --git a/com.unity.netcode.gameobjects/Tests/Editor/com.unity.netcode.editortests.asmdef b/com.unity.netcode.gameobjects/Tests/Editor/com.unity.netcode.editortests.asmdef index b2c942a948..56f600746b 100644 --- a/com.unity.netcode.gameobjects/Tests/Editor/com.unity.netcode.editortests.asmdef +++ b/com.unity.netcode.gameobjects/Tests/Editor/com.unity.netcode.editortests.asmdef @@ -33,6 +33,16 @@ "name": "Unity", "expression": "(0,2022.2.0a5)", "define": "UNITY_UNET_PRESENT" + }, + { + "name": "com.unity.transport", + "expression": "2.4.0", + "define": "UTP_TRANSPORT_2_4_ABOVE" + }, + { + "name": "Unity", + "expression": "6000.1.0a1", + "define": "HOSTNAME_RESOLUTION_AVAILABLE" } ] } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportConnectionTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportConnectionTests.cs index c0a13e2f0d..2771ae6299 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportConnectionTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportConnectionTests.cs @@ -30,6 +30,7 @@ public void OneTimeSetup() [UnityTearDown] public IEnumerator Cleanup() { + VerboseDebug = false; if (m_Server) { m_Server.Shutdown(); @@ -66,8 +67,20 @@ public void DetectInvalidEndpoint() m_Clients[0].ConnectionData.Address = "MoreFubar"; Assert.False(m_Server.StartServer(), "Server failed to detect invalid endpoint!"); Assert.False(m_Clients[0].StartClient(), "Client failed to detect invalid endpoint!"); +#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE + LogAssert.Expect(LogType.Error, $"Listen network address ({m_Server.ConnectionData.Address}) is not a valid {Networking.Transport.NetworkFamily.Ipv4} or {Networking.Transport.NetworkFamily.Ipv6} address!"); + LogAssert.Expect(LogType.Error, $"Target server network address ({m_Clients[0].ConnectionData.Address}) is not a valid Fully Qualified Domain Name!"); + + m_Server.ConnectionData.Address = "my.fubar.com"; + m_Server.ConnectionData.ServerListenAddress = "my.fubar.com"; + Assert.False(m_Server.StartServer(), "Server failed to detect invalid endpoint!"); + LogAssert.Expect(LogType.Error, $"While ({m_Server.ConnectionData.Address}) is a valid Fully Qualified Domain Name, you must use a " + + $"valid {Networking.Transport.NetworkFamily.Ipv4} or {Networking.Transport.NetworkFamily.Ipv6} address when binding and listening for connections!"); +#else netcodeLogAssert.LogWasReceived(LogType.Error, $"Network listen address ({m_Server.ConnectionData.Address}) is Invalid!"); netcodeLogAssert.LogWasReceived(LogType.Error, $"Target server network address ({m_Clients[0].ConnectionData.Address}) is Invalid!"); +#endif + UnityTransportTestComponent.CleanUp(); } @@ -194,26 +207,32 @@ public IEnumerator ClientDisconnectSingleClient() [UnityTest] public IEnumerator ClientDisconnectMultipleClients() { - InitializeTransport(out m_Server, out m_ServerEvents); - m_Server.StartServer(); + VerboseDebug = true; + InitializeTransport(out m_Server, out m_ServerEvents, identifier: "Server"); + Assert.True(m_Server.StartServer(), "Failed to start server!"); + for (int i = 0; i < k_NumClients; i++) { - InitializeTransport(out m_Clients[i], out m_ClientsEvents[i]); - m_Clients[i].StartClient(); + InitializeTransport(out m_Clients[i], out m_ClientsEvents[i], identifier: $"Client-{i + 1}"); + Assert.True(m_Clients[i].StartClient(), $"Failed to start client-{i + 1}"); + // Assure all clients have connected before disconnecting them + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[i], 5); } - yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[k_NumClients - 1]); // Disconnect a single client. + VerboseLog($"Disconnecting Client-1"); m_Clients[0].DisconnectLocalClient(); - yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ServerEvents); + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ServerEvents, 5); // Disconnect all the other clients. for (int i = 1; i < k_NumClients; i++) { + VerboseLog($"Disconnecting Client-{i + 1}"); m_Clients[i].DisconnectLocalClient(); } - yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ServerEvents, 5); + + yield return WaitForMultipleNetworkEvents(NetworkEvent.Disconnect, m_ServerEvents, 4, 20); // Check that we got the correct number of Disconnect events on the server. Assert.AreEqual(k_NumClients * 2, m_ServerEvents.Count); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportTestHelpers.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportTestHelpers.cs index 926b2fa7bc..91c33c0d7d 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportTestHelpers.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportTestHelpers.cs @@ -19,21 +19,63 @@ internal static class UnityTransportTestHelpers // Wait for an event to appear in the given event list (must be the very next event). public static IEnumerator WaitForNetworkEvent(NetworkEvent type, List events, float timeout = MaxNetworkEventWaitTime) { - int initialCount = events.Count; - float startTime = Time.realtimeSinceStartup; - - while (Time.realtimeSinceStartup - startTime < timeout) + var initialCount = events.Count; + var startTime = Time.realtimeSinceStartup + timeout; + var waitPeriod = new WaitForSeconds(0.01f); + var conditionMet = false; + while (startTime > Time.realtimeSinceStartup) { if (events.Count > initialCount) { Assert.AreEqual(type, events[initialCount].Type); - yield break; + conditionMet = true; + break; } - yield return new WaitForSeconds(0.01f); + yield return waitPeriod; + } + if (!conditionMet) + { + Assert.Fail("Timed out while waiting for network event."); } + } - Assert.Fail("Timed out while waiting for network event."); + internal static IEnumerator WaitForMultipleNetworkEvents(NetworkEvent type, List events, int count, float timeout = MaxNetworkEventWaitTime) + { + var initialCount = events.Count; + var startTime = Time.realtimeSinceStartup + timeout; + var waitPeriod = new WaitForSeconds(0.01f); + var conditionMet = false; + while (startTime > Time.realtimeSinceStartup) + { + // Wait until we have received at least (count) number of events + if ((events.Count - initialCount) >= count) + { + var foundTypes = 0; + // Look through all events received to match against the type we + // are looking for. + for (int i = initialCount; i < initialCount + count; i++) + { + if (type.Equals(events[i].Type)) + { + foundTypes++; + } + } + // If we reached the number of events we were expecting + conditionMet = foundTypes == count; + if (conditionMet) + { + // break from the wait loop + break; + } + } + + yield return waitPeriod; + } + if (!conditionMet) + { + Assert.Fail("Timed out while waiting for network event."); + } } // Wait to ensure no event is sent. @@ -53,12 +95,22 @@ public static IEnumerator EnsureNoNetworkEvent(List events, floa } } - // Common code to initialize a UnityTransport that logs its events. - public static void InitializeTransport(out UnityTransport transport, out List events, + public static void InitializeTransport(out UnityTransport transport, out List events, int maxPayloadSize = UnityTransport.InitialMaxPayloadSize, int maxSendQueueSize = 0, NetworkFamily family = NetworkFamily.Ipv4) + { + InitializeTransport(out transport, out events, string.Empty, maxPayloadSize, maxSendQueueSize, family); + } + + /// + /// Interanl version with identifier parameter + /// + internal static void InitializeTransport(out UnityTransport transport, out List events, string identifier, int maxPayloadSize = UnityTransport.InitialMaxPayloadSize, int maxSendQueueSize = 0, NetworkFamily family = NetworkFamily.Ipv4) { - var logger = new TransportEventLogger(); + var logger = new TransportEventLogger() + { + Identifier = identifier, + }; events = logger.Events; transport = new GameObject().AddComponent(); @@ -75,6 +127,16 @@ public static void InitializeTransport(out UnityTransport transport, out List m_Events = new List(); public List Events => m_Events; + + public string Identifier; public void HandleEvent(NetworkEvent type, ulong clientID, ArraySegment data, float receiveTime) { + VerboseLog($"[{Identifier}]Tansport Event][{type}][{receiveTime}] Client-{clientID}"); + // Copy the data since the backing array will be reused for future messages. if (data != default(ArraySegment)) { diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/com.unity.netcode.runtimetests.asmdef b/com.unity.netcode.gameobjects/Tests/Runtime/com.unity.netcode.runtimetests.asmdef index ce8ab1c7eb..7153a15480 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/com.unity.netcode.runtimetests.asmdef +++ b/com.unity.netcode.gameobjects/Tests/Runtime/com.unity.netcode.runtimetests.asmdef @@ -48,6 +48,16 @@ "name": "com.unity.modules.physics", "expression": "", "define": "COM_UNITY_MODULES_PHYSICS" + }, + { + "name": "com.unity.transport", + "expression": "2.4.0", + "define": "UTP_TRANSPORT_2_4_ABOVE" + }, + { + "name": "Unity", + "expression": "6000.1.0a1", + "define": "HOSTNAME_RESOLUTION_AVAILABLE" } ], "noEngineReferences": false