Skip to content
2 changes: 2 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -317,10 +320,14 @@ private static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port, bool
if (!NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv4) &&
!NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv6))
{
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
return default;
#else // If the user does not have the most recent version of UnityTransport installed
if (!silent)
{
Debug.LogError($"Invalid network endpoint: {ip}:{port}.");
}
#endif
}

return endpoint;
Expand Down Expand Up @@ -517,6 +524,16 @@ private NetworkPipeline SelectSendPipeline(NetworkDelivery delivery)
}
}

#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
private bool IsValidFqdn(string fqdn)
{
// Regular expression to validate FQDN
string pattern = @"^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.(?!-)(?:[A-Za-z0-9-]{1,63}\.?)+[A-Za-z]{2,6}$";
var regex = new Regex(pattern);
return regex.IsMatch(fqdn);
}
#endif

private bool ClientBindAndConnect()
{
var serverEndpoint = default(NetworkEndpoint);
Expand All @@ -539,11 +556,31 @@ private bool ClientBindAndConnect()
serverEndpoint = ConnectionData.ServerEndPoint;
}

NetworkConnection serverConnection;

// Verify the endpoint is valid before proceeding
if (serverEndpoint.Family == NetworkFamily.Invalid)
{
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
// If it's not valid, assure it meets FQDN standards
if (IsValidFqdn(ConnectionData.Address))
{
// If so, then proceed with driver initialization and attempt to connect
InitDriver();
serverConnection = m_Driver.Connect(ConnectionData.Address, ConnectionData.Port);
m_ServerClientId = ParseClientId(serverConnection);
return true;
}
else
{
// If not then log an error and return false
Debug.LogError($"Target server network address ({ConnectionData.Address}) is not a valid Fully Qualified Domain Name!");
return false;
}
#else
Debug.LogError($"Target server network address ({ConnectionData.Address}) is {nameof(NetworkFamily.Invalid)}!");
return false;
#endif
}

InitDriver();
Expand All @@ -556,7 +593,7 @@ private bool ClientBindAndConnect()
return false;
}

var serverConnection = m_Driver.Connect(serverEndpoint);
serverConnection = m_Driver.Connect(serverEndpoint);
m_ServerClientId = ParseClientId(serverConnection);

return true;
Expand All @@ -567,8 +604,22 @@ private bool ServerBindAndListen(NetworkEndpoint endPoint)
// Verify the endpoint is valid before proceeding
if (endPoint.Family == NetworkFamily.Invalid)
{
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
// If it's not valid, assure it meets FQDN standards
if (!IsValidFqdn(ConnectionData.Address))
{
// If not then log an error and return false
Debug.LogError($"Listen network address ({ConnectionData.Address}) is not a valid {NetworkFamily.Ipv4} or {NetworkFamily.Ipv6} address!");
}
else
{
Debug.LogError($"While ({ConnectionData.Address}) is a valid Fully Qualified Domain Name, you must use a valid {NetworkFamily.Ipv4} or {NetworkFamily.Ipv6} address when binding and listening for connections!");
}
return false;
#else
Debug.LogError($"Network listen address ({ConnectionData.Address}) is {nameof(NetworkFamily.Invalid)}!");
return false;
#endif
}

InitDriver();
Expand Down Expand Up @@ -647,7 +698,7 @@ public void SetClientRelayData(string ipAddress, ushort port, byte[] allocationI
/// <summary>
/// Sets IP and Port information. This will be ignored if using the Unity Relay and you should call <see cref="SetRelayServerData"/>
/// </summary>
/// <param name="ipv4Address">The remote IP address (despite the name, can be an IPv6 address)</param>
/// <param name="ipv4Address">The remote IP address (despite the name, can be an IPv6 address or a domain name)</param>
/// <param name="port">The remote port</param>
/// <param name="listenAddress">The local listen address</param>
public void SetConnectionData(string ipv4Address, ushort port, string listenAddress = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,12 @@ public void UnityTransport_RestartSucceedsAfterFailure()
transport.SetConnectionData("127.0.0.", 4242, "127.0.0.");

Assert.False(transport.StartServer());

#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, "Invalid network endpoint: 127.0.0.:4242.");
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());
Expand Down Expand Up @@ -150,9 +153,12 @@ public void UnityTransport_StartClientFailsWithBadAddress()

transport.SetConnectionData("foobar", 4242);
Assert.False(transport.StartClient());

#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, "Invalid network endpoint: foobar:4242.");
LogAssert.Expect(LogType.Error, "Target server network address (foobar) is Invalid!");
#endif

transport.Shutdown();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class UnityTransportConnectionTests
[UnityTearDown]
public IEnumerator Cleanup()
{
VerboseDebug = false;
if (m_Server)
{
m_Server.Shutdown();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TransportEvent> 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<TransportEvent> 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.
Expand All @@ -54,10 +96,21 @@ public static IEnumerator EnsureNoNetworkEvent(List<TransportEvent> events, floa
}

// Common code to initialize a UnityTransport that logs its events.
public static void InitializeTransport(out UnityTransport transport, out List<TransportEvent> events,
public static void InitializeTransport(out UnityTransport transport, out List<TransportEvent> events, int maxPayloadSize = UnityTransport.InitialMaxPayloadSize, int maxSendQueueSize = 0, NetworkFamily family = NetworkFamily.Ipv4)
{
InitializeTransport(out transport, out events, string.Empty, maxPayloadSize, maxSendQueueSize, family);
}

/// <summary>
/// Interanl version with identifier parameter
/// </summary>
internal static void InitializeTransport(out UnityTransport transport, out List<TransportEvent> 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<UnityTransport>();
Expand All @@ -74,6 +127,16 @@ public static void InitializeTransport(out UnityTransport transport, out List<Tr
transport.Initialize();
}

internal static bool VerboseDebug = false;

internal static void VerboseLog(string msg)
{
if (VerboseDebug)
{
Debug.Log($"{msg}");
}
}

// Information about an event generated by a transport (basically just the parameters that
// are normally passed along to a TransportEventDelegate).
public struct TransportEvent
Expand All @@ -90,8 +153,10 @@ public class TransportEventLogger
{
private readonly List<TransportEvent> m_Events = new List<TransportEvent>();
public List<TransportEvent> Events => m_Events;
internal string Identifier;
public void HandleEvent(NetworkEvent type, ulong clientID, ArraySegment<byte> 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<byte>))
{
Expand Down
Loading