diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 4e011d8b95..33da2cb693 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,6 +10,8 @@ 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. (#3440) + ### Fixed - Fixed issue where when a client changes ownership via RPC the `NetworkBehaviour.OnOwnershipChanged` can result in identical previous and current owner identifiers. (#3434) diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs index c910b679f1..2665214c9f 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 UnityEngine; using NetcodeNetworkEvent = Unity.Netcode.NetworkEvent; using TransportNetworkEvent = Unity.Networking.Transport.NetworkEvent; @@ -310,26 +313,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. @@ -338,27 +356,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; } } - public bool IsIpv6 => !string.IsNullOrEmpty(Address) && ParseNetworkEndpoint(Address, Port, true).Family == NetworkFamily.Ipv6; + /// + /// Returns true if the end point address is of type . + /// + public bool IsIpv6 => !string.IsNullOrEmpty(Address) && NetworkEndpoint.TryParse(Address, Port, out NetworkEndpoint _, NetworkFamily.Ipv6); } @@ -517,6 +543,16 @@ private NetworkPipeline SelectSendPipeline(NetworkDelivery delivery) } } +#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 3b673da866..2f2005a1cb 100644 --- a/com.unity.netcode.gameobjects/Runtime/com.unity.netcode.runtime.asmdef +++ b/com.unity.netcode.gameobjects/Runtime/com.unity.netcode.runtime.asmdef @@ -42,10 +42,20 @@ "expression": "2.1.0", "define": "UTP_TRANSPORT_2_1_ABOVE" }, + { + "name": "com.unity.transport", + "expression": "2.4.0", + "define": "UTP_TRANSPORT_2_4_ABOVE" + }, { "name": "Unity", "expression": "2023", "define": "UNITY_DEDICATED_SERVER_ARGUMENTS_PRESENT" + }, + { + "name": "Unity", + "expression": "6000.1.0a1", + "define": "HOSTNAME_RESOLUTION_AVAILABLE" } ] } diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Transports/UnityTransportTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/Transports/UnityTransportTests.cs index fb61c053a1..d065acb781 100644 --- a/com.unity.netcode.gameobjects/Tests/Editor/Transports/UnityTransportTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Editor/Transports/UnityTransportTests.cs @@ -120,7 +120,12 @@ public void UnityTransport_RestartSucceedsAfterFailure() Assert.False(transport.StartServer()); LogAssert.Expect(LogType.Error, "Invalid network endpoint: 127.0.0.:4242."); + +#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE + LogAssert.Expect(LogType.Error, $"Listen network address (127.0.0.) is not a valid {Networking.Transport.NetworkFamily.Ipv4} or {Networking.Transport.NetworkFamily.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()); @@ -150,9 +155,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 56d2377536..26172db1d6 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 @@ -38,6 +38,16 @@ "name": "com.unity.transport", "expression": "2.0.0-exp", "define": "UTP_TRANSPORT_2_0_ABOVE" + }, + { + "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 8effb7096c..7daa7495f0 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportConnectionTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportConnectionTests.cs @@ -22,6 +22,7 @@ public class UnityTransportConnectionTests [UnityTearDown] public IEnumerator Cleanup() { + VerboseDebug = false; if (m_Server) { m_Server.Shutdown(); @@ -57,8 +58,19 @@ 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 } // Check connection with a single client. @@ -186,35 +198,36 @@ 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); + 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); Assert.AreEqual(k_NumClients, m_ServerEvents.Count(e => e.Type == NetworkEvent.Disconnect)); - - yield return null; } // Check that server re-disconnects are no-ops. diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportTestHelpers.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Transports/UnityTransportTestHelpers.cs index aeb25434fc..2bade280fb 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 @@ public 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."); } + } + + 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; + } + } - Assert.Fail("Timed out while waiting for network event."); + yield return waitPeriod; + } + if (!conditionMet) + { + Assert.Fail("Timed out while waiting for network event."); + } } // Wait to ensure no event is sent. @@ -54,10 +96,21 @@ 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(); @@ -74,6 +127,16 @@ public static void InitializeTransport(out UnityTransport transport, out List m_Events = new List(); public List Events => m_Events; + internal 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 72d85ca4c5..642f0dd3bc 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 @@ -53,6 +53,16 @@ "name": "com.unity.transport", "expression": "2.0.0-exp", "define": "UTP_TRANSPORT_2_0_ABOVE" + }, + { + "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