Skip to content

fix: Accept single words as valid hostnames #3591

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: develop-2.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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