Skip to content
Merged
1 change: 1 addition & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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 Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
Expand Down Expand Up @@ -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}.");
}

/// <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 @@ -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;
}
}

/// <summary>
/// Returns true if the end point address is of type <see cref="NetworkFamily.Ipv6"/>.
/// </summary>
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);
}


Expand Down Expand Up @@ -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}(?<!-)\.(?!-)(?:[A-Za-z0-9-]{1,63}\.?)+[A-Za-z]{2,6}$";
var regex = new Regex(pattern);
return regex.IsMatch(fqdn);
}
#endif

private bool ClientBindAndConnect()
{
Expand All @@ -512,8 +544,26 @@ private bool ClientBindAndConnect()
// 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();
m_Driver.Connect(ConnectionData.Address, ConnectionData.Port);
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 Down Expand Up @@ -546,8 +596,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 @@ -625,7 +689,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 @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public void OneTimeSetup()
[UnityTearDown]
public IEnumerator Cleanup()
{
VerboseDebug = false;
if (m_Server)
{
m_Server.Shutdown();
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);
Expand Down
Loading