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
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 @@ -256,10 +259,14 @@ private static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port, bool
if (!NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv4) &&
!NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv6))
{
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
return default;
#else // If the user does not have the most recent version of UnityTransport installed
if (!silent)
{
Debug.LogError($"Invalid network endpoint: {ip}:{port}.");
}
#endif
}

return endpoint;
Expand Down Expand Up @@ -486,6 +493,15 @@ private NetworkPipeline SelectSendPipeline(NetworkDelivery delivery)
return NetworkPipeline.Null;
}
}
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
private 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 +528,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 +580,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 +673,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,12 @@ public void UnityTransport_RestartSucceedsAfterFailure()
transport.SetConnectionData("127.0.0.", 4242, "127.0.0.");

Assert.False(transport.StartServer());

#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, "Invalid network endpoint: 127.0.0.:4242.");
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 +164,12 @@ public void UnityTransport_StartClientFailsWithBadAddress()

transport.SetConnectionData("foobar", 4242);
Assert.False(transport.StartClient());

#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, "Invalid network endpoint: foobar:4242.");
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 @@ -22,6 +22,7 @@ internal class UnityTransportConnectionTests
[UnityTearDown]
public IEnumerator Cleanup()
{
VerboseDebug = false;
if (m_Server)
{
m_Server.Shutdown();
Expand Down Expand Up @@ -58,8 +59,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 @@ -186,26 +199,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
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,63 @@ internal static class UnityTransportTestHelpers
// Wait for an event to appear in the given event list (must be the very next event).
public static IEnumerator WaitForNetworkEvent(NetworkEvent type, List<TransportEvent> events, float timeout = MaxNetworkEventWaitTime)
{
int initialCount = events.Count;
float startTime = Time.realtimeSinceStartup;

while (Time.realtimeSinceStartup - startTime < timeout)
var initialCount = events.Count;
var startTime = Time.realtimeSinceStartup + timeout;
var waitPeriod = new WaitForSeconds(0.01f);
var conditionMet = false;
while (startTime > Time.realtimeSinceStartup)
{
if (events.Count > initialCount)
{
Assert.AreEqual(type, events[initialCount].Type);
yield break;
conditionMet = true;
break;
}

yield return new WaitForSeconds(0.01f);
yield return waitPeriod;
}
if (!conditionMet)
{
Assert.Fail("Timed out while waiting for network event.");
}
}

Assert.Fail("Timed out while waiting for network event.");
internal static IEnumerator WaitForMultipleNetworkEvents(NetworkEvent type, List<TransportEvent> events, int count, float timeout = MaxNetworkEventWaitTime)
{
var initialCount = events.Count;
var startTime = Time.realtimeSinceStartup + timeout;
var waitPeriod = new WaitForSeconds(0.01f);
var conditionMet = false;
while (startTime > Time.realtimeSinceStartup)
{
// Wait until we have received at least (count) number of events
if ((events.Count - initialCount) >= count)
{
var foundTypes = 0;
// Look through all events received to match against the type we
// are looking for.
for (int i = initialCount; i < initialCount + count; i++)
{
if (type.Equals(events[i].Type))
{
foundTypes++;
}
}
// If we reached the number of events we were expecting
conditionMet = foundTypes == count;
if (conditionMet)
{
// break from the wait loop
break;
}
}

yield return waitPeriod;
}
if (!conditionMet)
{
Assert.Fail("Timed out while waiting for network event.");
}
}

// Wait to ensure no event is sent.
Expand All @@ -53,12 +95,22 @@ public static IEnumerator EnsureNoNetworkEvent(List<TransportEvent> events, floa
}
}


// Common code to initialize a UnityTransport that logs its events.
public static void InitializeTransport(out UnityTransport transport, out List<TransportEvent> events,
public static void InitializeTransport(out UnityTransport transport, out List<TransportEvent> events, int maxPayloadSize = UnityTransport.InitialMaxPayloadSize, int maxSendQueueSize = 0, NetworkFamily family = NetworkFamily.Ipv4)
{
InitializeTransport(out transport, out events, string.Empty, maxPayloadSize, maxSendQueueSize, family);
}

/// <summary>
/// Interanl version with identifier parameter
/// </summary>
internal static void InitializeTransport(out UnityTransport transport, out List<TransportEvent> events, string identifier,
int maxPayloadSize = UnityTransport.InitialMaxPayloadSize, int maxSendQueueSize = 0, NetworkFamily family = NetworkFamily.Ipv4)
{
var logger = new TransportEventLogger();
var logger = new TransportEventLogger()
{
Identifier = identifier,
};
events = logger.Events;

transport = new GameObject().AddComponent<UnityTransportTestComponent>();
Expand All @@ -75,6 +127,16 @@ public static void InitializeTransport(out UnityTransport transport, out List<Tr
transport.Initialize();
}

public static bool VerboseDebug = false;

public static void VerboseLog(string msg)
{
if (VerboseDebug)
{
Debug.Log($"{msg}");
}
}

// Information about an event generated by a transport (basically just the parameters that
// are normally passed along to a TransportEventDelegate).
internal struct TransportEvent
Expand All @@ -91,8 +153,12 @@ internal class TransportEventLogger
{
private readonly List<TransportEvent> m_Events = new List<TransportEvent>();
public List<TransportEvent> Events => m_Events;

public string Identifier;
public void HandleEvent(NetworkEvent type, ulong clientID, ArraySegment<byte> data, float receiveTime)
{
VerboseLog($"[{Identifier}]Tansport Event][{type}][{receiveTime}] Client-{clientID}");

// Copy the data since the backing array will be reused for future messages.
if (data != default(ArraySegment<byte>))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@
"name": "com.unity.modules.physics",
"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",
"define": "HOSTNAME_RESOLUTION_AVAILABLE"
}
],
"noEngineReferences": false
Expand Down