Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

- Fixed an issue where `UnityTransport` would not accept single words as valid hostnames (notably "localhost"). (#3591)
- Fixed issue where viewing a `NetworkBehaviour` with one or more `NetworkVariable` fields could throw an exception if running a distributed authority network topology with a local (DAHost) host and viewed on the host when the host is not the authority of the associated `NetworkObject`. (#3578)
- Fixed issue when using a distributed authority network topology and viewing a `NetworkBehaviour` with one or more `NetworkVariable` fields in the inspector view would not show editable fields. (#3578)

### Changed

- Marked `UnityTransport.ConnectionAddressData.ServerEndPoint` as obsolete. It can't work when using hostnames as the server address, and its functionality can easily be replicated using `NetworkEndpoint.Parse`. (#3591)
- Optimized `NetworkList<T>` indexer setter to skip operations when the new value equals the existing value, improving performance by avoiding unnecessary list events and network synchronization. (#3587)


## [2.5.0] - 2025-08-01

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@

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 @@ -235,7 +232,7 @@ public struct ConnectionAddressData
[SerializeField]
public string ServerListenAddress;

private static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port)
internal static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port)
{
NetworkEndpoint endpoint = default;
if (!NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv4))
Expand All @@ -245,31 +242,16 @@ private static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port)
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
{
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;
}
}
/// <remarks>
/// If a DNS hostname was set as the address, this will return an invalid endpoint. This
/// is still handled correctly by NGO, but for this reason usage of this property is
/// discouraged.
/// </remarks>
[Obsolete("Use NetworkEndpoint.Parse on the Address field instead.")]
public NetworkEndpoint ServerEndPoint => ParseNetworkEndpoint(Address, Port);

/// <summary>
/// Endpoint (IP address and port) server will listen/bind on.
Expand All @@ -281,32 +263,27 @@ public NetworkEndpoint ListenEndPoint
NetworkEndpoint endpoint = default;
if (string.IsNullOrEmpty(ServerListenAddress))
{
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)
{
endpoint = NetworkEndpoint.LoopbackIpv6;
}
endpoint = IsIpv6 ? NetworkEndpoint.LoopbackIpv6 : NetworkEndpoint.LoopbackIpv4;
endpoint = endpoint.WithPort(Port);
}
else
{
endpoint = ParseNetworkEndpoint(ServerListenAddress, Port);
if (endpoint == default)
{
InvalidEndpointError();
Debug.LogError($"Invalid listen endpoint: {ServerListenAddress}:{Port}. Note that the listen endpoint MUST be an IP address (not a hostname).");
}
}
return endpoint;
}
}

/// <summary>
/// Returns true if the end point address is of type <see cref="NetworkFamily.Ipv6"/>.
/// Returns true if the end point address is of type <see cref="NetworkFamily.Ipv6"/> or
/// if it is a hostname (because in current versions of the engine, hostname resolution
/// prioritizes IPv6 addresses).
/// </summary>
public bool IsIpv6 => !string.IsNullOrEmpty(Address) && NetworkEndpoint.TryParse(Address, Port, out NetworkEndpoint _, NetworkFamily.Ipv6);
public bool IsIpv6 => !string.IsNullOrEmpty(Address) && !NetworkEndpoint.TryParse(Address, Port, out NetworkEndpoint _, NetworkFamily.Ipv4);
}


Expand Down Expand Up @@ -667,16 +644,6 @@ 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 @@ -687,44 +654,20 @@ private bool ClientBindAndConnect()
}
else
{
serverEndpoint = ConnectionData.ServerEndPoint;
}

// 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
// This will result in an invalid endpoint if the address is a hostname.
// This is handled later in the Connect method if hostname resolution is available,
// but if not then we need to error out here.
serverEndpoint = ConnectionAddressData.ParseNetworkEndpoint(ConnectionData.Address, ConnectionData.Port);
#if !HOSTNAME_RESOLUTION_AVAILABLE
if (serverEndpoint.Family == NetworkFamily.Invalid)
{
// 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!");
Debug.LogError($"Invalid server address: {ConnectionData.Address}:{ConnectionData.Port}.");
return false;
}
#else
Debug.LogError($"Target server network address ({ConnectionData.Address}) is {nameof(NetworkFamily.Invalid)}!");
return false;
#endif
}

InitDriver();

var bindEndpoint = serverEndpoint.Family == NetworkFamily.Ipv6 ? NetworkEndpoint.AnyIpv6 : NetworkEndpoint.AnyIpv4;
int result = m_Driver.Bind(bindEndpoint);
if (result != 0)
{
Debug.LogError("Client failed to bind");
return false;
}

Connect(serverEndpoint);

return true;
Expand All @@ -737,30 +680,22 @@ private bool ClientBindAndConnect()
/// <returns>A <see cref="NetworkConnection"/> representing the connection to the server, or an invalid connection if the connection attempt fails.</returns>
protected virtual NetworkConnection Connect(NetworkEndpoint serverEndpoint)
{
#if HOSTNAME_RESOLUTION_AVAILABLE
// If the server endpoint is invalid, it means whatever the user entered in the address
// field was not an IP address, and must be presumed to be a hostname.
if (serverEndpoint.Family == NetworkFamily.Invalid)
{
return m_Driver.Connect(ConnectionData.Address, ConnectionData.Port);
}
#endif
return m_Driver.Connect(serverEndpoint);
}

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
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,6 @@
"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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,11 @@ public void UnityTransport_RestartSucceedsAfterFailure()
UnityTransport transport = new GameObject().AddComponent<UnityTransport>();
transport.Initialize();

transport.SetConnectionData("127.0.0.", 4242, "127.0.0.");
transport.SetConnectionData("127.0.0.1", 4242, "foobar");

Assert.False(transport.StartServer());
LogAssert.Expect(LogType.Error, "Invalid network endpoint: 127.0.0.:4242.");
LogAssert.Expect(LogType.Error, "Invalid listen endpoint: foobar:4242. Note that the listen endpoint MUST be an IP address (not a hostname).");

#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 All @@ -156,24 +151,6 @@ public void UnityTransport_StartServerWithoutAddresses()
transport.Shutdown();
}

// Check that StartClient returns false with bad connection data.
[Test]
public void UnityTransport_StartClientFailsWithBadAddress()
{
UnityTransport transport = new GameObject().AddComponent<UnityTransport>();
transport.Initialize();

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();
}

[Test]
public void UnityTransport_EmptySecurityStringsShouldThrow([Values("", null)] string cert, [Values("", null)] string secret)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@
"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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using Unity.Netcode.Transports.UTP;
using Unity.Networking.Transport;
using UnityEngine;
using UnityEngine.TestTools;
using static Unity.Netcode.RuntimeTests.UnityTransportTestHelpers;
Expand Down Expand Up @@ -55,45 +56,52 @@ public IEnumerator Cleanup()
yield return null;
}

// Check that invalid endpoint addresses are detected and return false if detected
[Test]
public void DetectInvalidEndpoint()
// Check connection with a single client (IP address).
[UnityTest]
public IEnumerator ConnectSingleClient_IPAddress()
{
using var netcodeLogAssert = new NetcodeLogAssert(true);
InitializeTransport(out m_Server, out m_ServerEvents);
InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]);
m_Server.ConnectionData.Address = "Fubar";
m_Server.ConnectionData.ServerListenAddress = "Fubar";
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();
m_Clients[0].SetConnectionData("127.0.0.1", 7777);

m_Server.StartServer();
m_Clients[0].StartClient();

yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[0]);

// Check we've received Connect event on server too.
Assert.AreEqual(1, m_ServerEvents.Count);
Assert.AreEqual(NetworkEvent.Connect, m_ServerEvents[0].Type);

yield return null;
}

// Check connection with a single client.
#if HOSTNAME_RESOLUTION_AVAILABLE
// Check connection with a single client (hostname).
[UnityTest]
public IEnumerator ConnectSingleClient()
public IEnumerator ConnectSingleClient_Hostname()
{
InitializeTransport(out m_Server, out m_ServerEvents);
InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]);

m_Server.StartServer();
// We don't know if localhost will resolve to 127.0.0.1 or ::1, so we wait until we know
// before starting the server. Because localhost is pretty much always defined locally
// it should resolve immediatly and thus waiting one frame should be enough.

// We'll need to retry connection requests most likely so make this fast.
m_Clients[0].ConnectTimeoutMS = 50;

m_Clients[0].SetConnectionData("localhost", 7777);
m_Clients[0].StartClient();

yield return null;

var endpoint = m_Clients[0].GetLocalEndpoint();
var ip = endpoint.Family == NetworkFamily.Ipv4 ? "127.0.0.1" : "::1";
m_Server.SetConnectionData(ip, 7777, ip);
m_Server.StartServer();

yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[0]);

// Check we've received Connect event on server too.
Expand All @@ -102,6 +110,7 @@ public IEnumerator ConnectSingleClient()

yield return null;
}
#endif

// Check connection with multiple clients.
[UnityTest]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@
"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",
Expand Down