Skip to content

Commit b21d4ad

Browse files
simon-lemay-unitymichalChrobotNoelStephensUnity
authored
fix: Accept single words as valid hostnames (#3591)
## Purpose of this PR The regex we used to validate hostnames did not accept single words as valid hostnames. But single words _can_ be valid hostnames. The most common is of course "localhost" but one can edit one's hosts file to define any word as something the local resolver will resolve to an IP address. This PR addresses this by removing any validation of the provided hostname. We could modify the validity check to accept single words too, but the regex is pretty indigestible already and it's simpler to just let the resolver fail if the user provided garbage. UTP will signal this through a disconnection event with an appropriate reason (which we'll be able to map to a nice error message once [this PR](#3551) lands). While I was at it, I also made a few cleanups and improvements: - Removed the `UTP_TRANSPORT_2_4_ABOVE` define. NGO depends on UTP 2.4.0 so it can be safely assumed that users will have it installed. No need to conditionally compile the code that depends on it. - Added a test that establishes a connection using hostname resolution. - Simplified the logic around connections and avoid bypassing the `Connect` method when connecting to a hostname. - Deprecated `ConnectionAddressData.ServerEndPoint`. We don't use it anymore, it doesn't work with hostnames, and it's not providing any value over just calling `NetworkEndpoint.Parse`. And worst of all: its capitalization of "endpoint" doesn't match what we use elsewhere. - Modified the listen address logic so that if a domain name is used, by default if remote connections are allowed we will listen on :: (the IPv6 "any" address) instead of 0.0.0.0. This can still be overridden using `SetConnectionData`. The reason for this change is that the resolver in UTP prioritizes IPv6 addresses over IPv4. So if we listen on IPv4 by default, we're likely to get issues if the resolver then ends up with an IPv6 address. In current versions of UTP for instance, this causes errors on Windows (a fix is on the way). I'm looking into changing the behavior of the resolver to prefer IPv4, but that's an engine change so might take a while to land. In the meantime defaulting to IPv6 seems like the best approach. ### Changelog - Fixed: Fixed an issue where `UnityTransport` would not accept single words as valid hostnames (notably "localhost"). - 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`. ## Documentation No documentation changes or additions were necessary. ## Testing & QA Tested with manual and automated tests. ## Backport Hostname resolution is only supported in UTP 2.4+ and Unity 6.1+, so no backport necessary. --------- Co-authored-by: Michał Chrobot <[email protected]> Co-authored-by: Noel Stephens <[email protected]>
1 parent 5f9463a commit b21d4ad

File tree

7 files changed

+108
-161
lines changed

7 files changed

+108
-161
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ Additional documentation and release notes are available at [Multiplayer Documen
1414

1515
### Fixed
1616

17+
- Fixed an issue where `UnityTransport` would not accept single words as valid hostnames (notably "localhost"). (#3591)
1718
- 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)
1819
- 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)
1920

2021
### Changed
2122

23+
- 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)
2224
- 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)
2325

24-
2526
## [2.5.0] - 2025-08-01
2627

2728
### Added

com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs

Lines changed: 37 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77

88
using System;
99
using System.Collections.Generic;
10-
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
11-
using System.Text.RegularExpressions;
12-
#endif
1310
using Unity.Burst;
1411
using Unity.Collections;
1512
using Unity.Collections.LowLevel.Unsafe;
@@ -235,7 +232,7 @@ public struct ConnectionAddressData
235232
[SerializeField]
236233
public string ServerListenAddress;
237234

238-
private static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port)
235+
internal static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port)
239236
{
240237
NetworkEndpoint endpoint = default;
241238
if (!NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv4))
@@ -245,31 +242,16 @@ private static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port)
245242
return endpoint;
246243
}
247244

248-
private void InvalidEndpointError()
249-
{
250-
Debug.LogError($"Invalid network endpoint: {Address}:{Port}.");
251-
}
252-
253245
/// <summary>
254246
/// Endpoint (IP address and port) clients will connect to.
255247
/// </summary>
256-
public NetworkEndpoint ServerEndPoint
257-
{
258-
get
259-
{
260-
var networkEndpoint = ParseNetworkEndpoint(Address, Port);
261-
if (networkEndpoint == default)
262-
{
263-
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
264-
if (!IsValidFqdn(Address))
265-
#endif
266-
{
267-
InvalidEndpointError();
268-
}
269-
}
270-
return networkEndpoint;
271-
}
272-
}
248+
/// <remarks>
249+
/// If a DNS hostname was set as the address, this will return an invalid endpoint. This
250+
/// is still handled correctly by NGO, but for this reason usage of this property is
251+
/// discouraged.
252+
/// </remarks>
253+
[Obsolete("Use NetworkEndpoint.Parse on the Address field instead.")]
254+
public NetworkEndpoint ServerEndPoint => ParseNetworkEndpoint(Address, Port);
273255

274256
/// <summary>
275257
/// Endpoint (IP address and port) server will listen/bind on.
@@ -281,32 +263,27 @@ public NetworkEndpoint ListenEndPoint
281263
NetworkEndpoint endpoint = default;
282264
if (string.IsNullOrEmpty(ServerListenAddress))
283265
{
284-
endpoint = NetworkEndpoint.LoopbackIpv4;
285-
286-
// If an address was entered and it's IPv6, switch to using ::1 as the
287-
// default listen address. (Otherwise we always assume IPv4.)
288-
if (!string.IsNullOrEmpty(Address) && ServerEndPoint.Family == NetworkFamily.Ipv6)
289-
{
290-
endpoint = NetworkEndpoint.LoopbackIpv6;
291-
}
266+
endpoint = IsIpv6 ? NetworkEndpoint.LoopbackIpv6 : NetworkEndpoint.LoopbackIpv4;
292267
endpoint = endpoint.WithPort(Port);
293268
}
294269
else
295270
{
296271
endpoint = ParseNetworkEndpoint(ServerListenAddress, Port);
297272
if (endpoint == default)
298273
{
299-
InvalidEndpointError();
274+
Debug.LogError($"Invalid listen endpoint: {ServerListenAddress}:{Port}. Note that the listen endpoint MUST be an IP address (not a hostname).");
300275
}
301276
}
302277
return endpoint;
303278
}
304279
}
305280

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

312289

@@ -667,16 +644,6 @@ private NetworkPipeline SelectSendPipeline(NetworkDelivery delivery)
667644
}
668645
}
669646

670-
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
671-
private static bool IsValidFqdn(string fqdn)
672-
{
673-
// Regular expression to validate FQDN
674-
string pattern = @"^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.(?!-)(?:[A-Za-z0-9-]{1,63}\.?)+[A-Za-z]{2,6}$";
675-
var regex = new Regex(pattern);
676-
return regex.IsMatch(fqdn);
677-
}
678-
#endif
679-
680647
private bool ClientBindAndConnect()
681648
{
682649
var serverEndpoint = default(NetworkEndpoint);
@@ -687,44 +654,28 @@ private bool ClientBindAndConnect()
687654
}
688655
else
689656
{
690-
serverEndpoint = ConnectionData.ServerEndPoint;
691-
}
692-
693-
// Verify the endpoint is valid before proceeding
694-
if (serverEndpoint.Family == NetworkFamily.Invalid)
695-
{
696-
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
657+
// This will result in an invalid endpoint if the address is a hostname.
658+
// This is handled later in the Connect method if hostname resolution is available
659+
// (although we still check for hostname validity to error out early if it's not),
660+
// but if not then we need to error out here.
661+
serverEndpoint = ConnectionAddressData.ParseNetworkEndpoint(ConnectionData.Address, ConnectionData.Port);
697662

698-
// If it's not valid, assure it meets FQDN standards
699-
if (IsValidFqdn(ConnectionData.Address))
663+
if (serverEndpoint.Family == NetworkFamily.Invalid)
700664
{
701-
// If so, then proceed with driver initialization and attempt to connect
702-
InitDriver();
703-
m_Driver.Connect(ConnectionData.Address, ConnectionData.Port);
704-
return true;
705-
}
706-
else
707-
{
708-
// If not then log an error and return false
709-
Debug.LogError($"Target server network address ({ConnectionData.Address}) is not a valid Fully Qualified Domain Name!");
710-
return false;
711-
}
665+
#if HOSTNAME_RESOLUTION_AVAILABLE
666+
if (Uri.CheckHostName(ConnectionData.Address) != UriHostNameType.Dns)
667+
{
668+
Debug.LogError($"Provided connection address \"{ConnectionData.Address}\" is not a valid hostname.");
669+
return false;
670+
}
712671
#else
713-
Debug.LogError($"Target server network address ({ConnectionData.Address}) is {nameof(NetworkFamily.Invalid)}!");
714-
return false;
672+
Debug.LogError($"Invalid server address: {ConnectionData.Address}:{ConnectionData.Port}.");
673+
return false;
715674
#endif
675+
}
716676
}
717677

718678
InitDriver();
719-
720-
var bindEndpoint = serverEndpoint.Family == NetworkFamily.Ipv6 ? NetworkEndpoint.AnyIpv6 : NetworkEndpoint.AnyIpv4;
721-
int result = m_Driver.Bind(bindEndpoint);
722-
if (result != 0)
723-
{
724-
Debug.LogError("Client failed to bind");
725-
return false;
726-
}
727-
728679
Connect(serverEndpoint);
729680

730681
return true;
@@ -737,30 +688,22 @@ private bool ClientBindAndConnect()
737688
/// <returns>A <see cref="NetworkConnection"/> representing the connection to the server, or an invalid connection if the connection attempt fails.</returns>
738689
protected virtual NetworkConnection Connect(NetworkEndpoint serverEndpoint)
739690
{
691+
#if HOSTNAME_RESOLUTION_AVAILABLE
692+
// If the server endpoint is invalid, it means whatever the user entered in the address
693+
// field was not an IP address, and must be presumed to be a hostname.
694+
if (serverEndpoint.Family == NetworkFamily.Invalid)
695+
{
696+
return m_Driver.Connect(ConnectionData.Address, ConnectionData.Port);
697+
}
698+
#endif
740699
return m_Driver.Connect(serverEndpoint);
741700
}
742701

743702
private bool ServerBindAndListen(NetworkEndpoint endPoint)
744703
{
745-
// Verify the endpoint is valid before proceeding
746704
if (endPoint.Family == NetworkFamily.Invalid)
747705
{
748-
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
749-
// If it's not valid, assure it meets FQDN standards
750-
if (!IsValidFqdn(ConnectionData.Address))
751-
{
752-
// If not then log an error and return false
753-
Debug.LogError($"Listen network address ({ConnectionData.Address}) is not a valid {NetworkFamily.Ipv4} or {NetworkFamily.Ipv6} address!");
754-
}
755-
else
756-
{
757-
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!");
758-
}
759706
return false;
760-
#else
761-
Debug.LogError($"Network listen address ({ConnectionData.Address}) is {nameof(NetworkFamily.Invalid)}!");
762-
return false;
763-
#endif
764707
}
765708

766709
InitDriver();

com.unity.netcode.gameobjects/Runtime/Unity.Netcode.Runtime.asmdef

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,6 @@
6868
"expression": "6000.0.11f1",
6969
"define": "COM_UNITY_MODULES_PHYSICS2D_LINEAR"
7070
},
71-
{
72-
"name": "com.unity.transport",
73-
"expression": "2.4.0",
74-
"define": "UTP_TRANSPORT_2_4_ABOVE"
75-
},
7671
{
7772
"name": "Unity",
7873
"expression": "6000.1.0a1",

com.unity.netcode.gameobjects/Tests/Editor/Transports/UnityTransportTests.cs

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,11 @@ public void UnityTransport_RestartSucceedsAfterFailure()
127127
UnityTransport transport = new GameObject().AddComponent<UnityTransport>();
128128
transport.Initialize();
129129

130-
transport.SetConnectionData("127.0.0.", 4242, "127.0.0.");
130+
transport.SetConnectionData("127.0.0.1", 4242, "foobar");
131131

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

135-
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
136-
LogAssert.Expect(LogType.Error, "Listen network address (127.0.0.) is not a valid Ipv4 or Ipv6 address!");
137-
#else
138-
LogAssert.Expect(LogType.Error, "Network listen address (127.0.0.) is Invalid!");
139-
#endif
140135
transport.SetConnectionData("127.0.0.1", 4242, "127.0.0.1");
141136
Assert.True(transport.StartServer());
142137

@@ -156,24 +151,6 @@ public void UnityTransport_StartServerWithoutAddresses()
156151
transport.Shutdown();
157152
}
158153

159-
// Check that StartClient returns false with bad connection data.
160-
[Test]
161-
public void UnityTransport_StartClientFailsWithBadAddress()
162-
{
163-
UnityTransport transport = new GameObject().AddComponent<UnityTransport>();
164-
transport.Initialize();
165-
166-
transport.SetConnectionData("foobar", 4242);
167-
Assert.False(transport.StartClient());
168-
LogAssert.Expect(LogType.Error, "Invalid network endpoint: foobar:4242.");
169-
#if HOSTNAME_RESOLUTION_AVAILABLE && UTP_TRANSPORT_2_4_ABOVE
170-
LogAssert.Expect(LogType.Error, "Target server network address (foobar) is not a valid Fully Qualified Domain Name!");
171-
#else
172-
LogAssert.Expect(LogType.Error, "Target server network address (foobar) is Invalid!");
173-
#endif
174-
transport.Shutdown();
175-
}
176-
177154
[Test]
178155
public void UnityTransport_EmptySecurityStringsShouldThrow([Values("", null)] string cert, [Values("", null)] string secret)
179156
{
@@ -206,5 +183,37 @@ public void UnityTransport_EmptySecurityStringsShouldThrow([Values("", null)] st
206183
}
207184
}
208185
}
186+
187+
#if HOSTNAME_RESOLUTION_AVAILABLE
188+
private static readonly (string, bool)[] k_HostnameChecks =
189+
{
190+
("localhost", true),
191+
("unity3d.com", true),
192+
("unity3d.com.", true),
193+
(string.Empty, false),
194+
("unity3d.com/test", false),
195+
("test%123.com", false),
196+
};
197+
198+
[Test]
199+
[TestCaseSource(nameof(k_HostnameChecks))]
200+
public void UnityTransport_HostnameValidation((string, bool) testCase)
201+
{
202+
var (hostname, isValid) = testCase;
203+
204+
UnityTransport transport = new GameObject().AddComponent<UnityTransport>();
205+
transport.Initialize();
206+
207+
if (!isValid)
208+
{
209+
LogAssert.Expect(LogType.Error, $"Provided connection address \"{hostname}\" is not a valid hostname.");
210+
}
211+
212+
transport.SetConnectionData(hostname, 4242);
213+
Assert.AreEqual(isValid, transport.StartClient());
214+
215+
transport.Shutdown();
216+
}
217+
#endif
209218
}
210219
}

com.unity.netcode.gameobjects/Tests/Editor/Unity.Netcode.Editor.Tests.asmdef

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,6 @@
3838
"expression": "(0,2022.2.0a5)",
3939
"define": "UNITY_UNET_PRESENT"
4040
},
41-
{
42-
"name": "com.unity.transport",
43-
"expression": "2.4.0",
44-
"define": "UTP_TRANSPORT_2_4_ABOVE"
45-
},
4641
{
4742
"name": "Unity",
4843
"expression": "6000.1.0a1",

0 commit comments

Comments
 (0)