Skip to content

feat: Add support for connecting via a hostname instead of IP #3441

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

Merged
merged 15 commits into from
May 15, 2025
Merged
1 change: 1 addition & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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;
@@ -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.
@@ -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);
}


@@ -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()
{
@@ -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();
@@ -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();
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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());

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

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

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