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 @@ -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}.");
}

/// <summary>
/// Endpoint (IP address and port) clients will connect to.
/// </summary>
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;
}
}

/// <summary>
/// Endpoint (IP address and port) server will listen/bind on.
Expand All @@ -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;
/// <summary>
/// Returns true if the end point address is of type <see cref="NetworkFamily.Ipv6"/>.
/// </summary>
public bool IsIpv6 => !string.IsNullOrEmpty(Address) && NetworkEndpoint.TryParse(Address, Port, out NetworkEndpoint _, NetworkFamily.Ipv6);
}


Expand Down Expand Up @@ -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}(?<!-)\.(?!-)(?:[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 +575,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 +612,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 +623,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 +717,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 @@ -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());
Expand Down Expand Up @@ -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();
}
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
Loading