Skip to content

Commit c41ce9b

Browse files
liveansCopilot
andauthored
[System.Net.NameResolution] Fall back to localhost when localhost. resolution fails (#130504)
## Summary Some OS resolvers fail to resolve the fully qualified `localhost.` name even though it is equivalent to `localhost`. This change makes `localhost.` eligible for the existing RFC 6761 fallback behavior: - Try resolving the original name through the OS first. - Fall back to plain `localhost` if resolution fails or returns no addresses. - Preserve the existing localhost subdomain behavior. Fixes #129204 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent e7f7027 commit c41ce9b

3 files changed

Lines changed: 35 additions & 28 deletions

File tree

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ private static bool ValidateAddressFamily(ref AddressFamily addressFamily, strin
417417
}
418418

419419
private const string Localhost = "localhost";
420+
private const string LocalhostWithTrailingDot = Localhost + ".";
420421
private const string InvalidDomain = "invalid";
421422

422423
// Some systems (e.g. Android, some Linux distros) map ::1 to "ip6-localhost" instead of
@@ -451,19 +452,25 @@ private static bool IsReservedName(string hostName, string reservedName)
451452
}
452453

453454
/// <summary>
454-
/// Checks if the given host name is a subdomain of localhost (e.g., "foo.localhost").
455-
/// Plain "localhost" or "localhost." returns false.
455+
/// Checks if the given host name should fall back to "localhost" when resolution fails or returns no addresses.
456+
/// Fully qualified "localhost." and localhost subdomains return true; plain "localhost" returns false.
456457
/// </summary>
457-
private static bool IsLocalhostSubdomain(string hostName)
458+
private static bool ShouldFallbackToLocalhost(string hostName)
458459
{
460+
// Plain "localhost" is the fallback target, so only its fully qualified form should enter this path.
461+
if (hostName.Equals(LocalhostWithTrailingDot, StringComparison.OrdinalIgnoreCase))
462+
{
463+
return true;
464+
}
465+
459466
// Strip trailing dot for length comparison
460467
int length = hostName.Length;
461468
if (hostName.EndsWith('.'))
462469
{
463470
length--;
464471
}
465472

466-
// Must be longer than "localhost" (not just equal with trailing dot)
473+
// Subdomains must be longer than "localhost".
467474
return length > Localhost.Length && IsReservedName(hostName, Localhost);
468475
}
469476

@@ -513,10 +520,10 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr
513520

514521
if (errorCode != SocketError.Success)
515522
{
516-
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
517-
if (IsLocalhostSubdomain(hostName))
523+
// RFC 6761 Section 6.3: If a localhost name fails, fall back to resolving plain "localhost".
524+
if (ShouldFallbackToLocalhost(hostName))
518525
{
519-
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
526+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost name resolution failed, falling back to 'localhost'");
520527
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: CreateException(errorCode, nativeErrorCode));
521528
fallbackToLocalhost = true;
522529
}
@@ -526,10 +533,10 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr
526533
throw CreateException(errorCode, nativeErrorCode);
527534
}
528535
}
529-
else if (addresses.Length == 0 && IsLocalhostSubdomain(hostName))
536+
else if (addresses.Length == 0 && ShouldFallbackToLocalhost(hostName))
530537
{
531-
// RFC 6761 Section 6.3: If localhost subdomain returns empty addresses, fall back to plain "localhost".
532-
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'");
538+
// RFC 6761 Section 6.3: If a localhost name returns empty addresses, fall back to plain "localhost".
539+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost name returned empty, falling back to 'localhost'");
533540
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: justAddresses ? addresses : (object)new IPHostEntry { AddressList = addresses, HostName = newHostName!, Aliases = aliases }, exception: null);
534541
fallbackToLocalhost = true;
535542
}
@@ -713,7 +720,7 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR
713720
Task.FromException<IPHostEntry>(invalidDomainException!);
714721
}
715722

716-
// For localhost subdomains (RFC 6761 Section 6.3), we try the OS resolver first.
723+
// For localhost subdomains and fully qualified "localhost." (RFC 6761 Section 6.3), we try the OS resolver first.
717724
// If it fails or returns empty, we fall back to resolving plain "localhost".
718725
// This fallback logic is handled in GetHostEntryOrAddressesCore and GetAddrInfoWithTelemetryAsync.
719726

@@ -725,11 +732,11 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR
725732
// instead of calling the synchronous version in the ThreadPool.
726733
// If it fails, we will fall back to ThreadPool as well.
727734

728-
// Always use the telemetry-enabled path for localhost subdomains to ensure fallback handling.
735+
// Always use the telemetry-enabled path for names requiring localhost fallback handling.
729736
// For other hostnames, use the non-telemetry path if diagnostics are disabled.
730-
bool isLocalhostSubdomain = IsLocalhostSubdomain(hostName);
737+
bool shouldFallbackToLocalhost = ShouldFallbackToLocalhost(hostName);
731738
Task? t;
732-
if (NameResolutionTelemetry.AnyDiagnosticsEnabled() || isLocalhostSubdomain)
739+
if (NameResolutionTelemetry.AnyDiagnosticsEnabled() || shouldFallbackToLocalhost)
733740
{
734741
t = justAddresses
735742
? GetAddrInfoWithTelemetryAsync<IPAddress[]>(hostName, justAddresses, family, cancellationToken)
@@ -783,15 +790,15 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR
783790

784791
if (task != null)
785792
{
786-
bool isLocalhostSubdomain = IsLocalhostSubdomain(hostName);
787-
return CompleteAsync(task, hostName, justAddresses, addressFamily, isLocalhostSubdomain, startingTimestamp, cancellationToken);
793+
bool shouldFallbackToLocalhost = ShouldFallbackToLocalhost(hostName);
794+
return CompleteAsync(task, hostName, justAddresses, addressFamily, shouldFallbackToLocalhost, startingTimestamp, cancellationToken);
788795
}
789796

790797
// If resolution even did not start don't bother with telemetry.
791798
// We will retry on thread-pool.
792799
return null;
793800

794-
static async Task<T> CompleteAsync(Task task, string hostName, bool justAddresses, AddressFamily addressFamily, bool isLocalhostSubdomain, long startingTimeStamp, CancellationToken cancellationToken)
801+
static async Task<T> CompleteAsync(Task task, string hostName, bool justAddresses, AddressFamily addressFamily, bool shouldFallbackToLocalhost, long startingTimeStamp, CancellationToken cancellationToken)
795802
{
796803
NameResolutionActivity activity = NameResolutionTelemetry.Log.BeforeResolution(hostName, startingTimeStamp);
797804
Exception? exception = null;
@@ -801,19 +808,19 @@ static async Task<T> CompleteAsync(Task task, string hostName, bool justAddresse
801808
{
802809
result = await ((Task<T>)task).ConfigureAwait(false);
803810

804-
// RFC 6761 Section 6.3: If localhost subdomain returns empty addresses, fall back to plain "localhost".
805-
if (isLocalhostSubdomain && result is IPAddress[] addresses && addresses.Length == 0)
811+
// RFC 6761 Section 6.3: If a localhost name returns empty addresses, fall back to plain "localhost".
812+
if (shouldFallbackToLocalhost && result is IPAddress[] addresses && addresses.Length == 0)
806813
{
807-
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'");
814+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost name returned empty, falling back to 'localhost'");
808815
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
809816
fallbackOccurred = true;
810817

811818
return await GetLocalhostAddressesAsync(addressFamily, cancellationToken).ConfigureAwait(false);
812819
}
813820

814-
if (isLocalhostSubdomain && result is IPHostEntry entry && entry.AddressList.Length == 0)
821+
if (shouldFallbackToLocalhost && result is IPHostEntry entry && entry.AddressList.Length == 0)
815822
{
816-
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'");
823+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost name returned empty, falling back to 'localhost'");
817824
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
818825
fallbackOccurred = true;
819826

@@ -822,10 +829,10 @@ static async Task<T> CompleteAsync(Task task, string hostName, bool justAddresse
822829

823830
return result;
824831
}
825-
catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
832+
catch (SocketException ex) when (shouldFallbackToLocalhost && !fallbackOccurred)
826833
{
827-
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
828-
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
834+
// RFC 6761 Section 6.3: If a localhost name fails, fall back to resolving plain "localhost".
835+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost name resolution failed, falling back to 'localhost'");
829836
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
830837
fallbackOccurred = true;
831838

src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ public async Task DnsGetHostAddresses_MalformedReservedName_NotTreatedAsReserved
370370
await Assert.ThrowsAnyAsync<Exception>(() => Dns.GetHostAddressesAsync(hostName));
371371
}
372372

373-
// "localhost." (with trailing dot) should NOT be treated as a subdomain.
373+
// "localhost." is resolved by the OS first, with fallback to plain "localhost" on failure or an empty result.
374374
[Fact]
375375
public async Task DnsGetHostAddresses_LocalhostWithTrailingDot_ReturnsLoopback()
376376
{

src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,8 +421,8 @@ public async Task DnsGetHostEntry_MalformedReservedName_NotTreatedAsReserved(str
421421
await Assert.ThrowsAnyAsync<Exception>(() => Dns.GetHostEntryAsync(hostName));
422422
}
423423

424-
// "localhost." (with trailing dot) should NOT be treated as a subdomain.
425-
// It's equivalent to plain "localhost" and should resolve via OS resolver.
424+
// "localhost." is equivalent to plain "localhost".
425+
// The OS resolver is tried first, falling back to plain "localhost" if resolution fails or returns no addresses.
426426
[Fact]
427427
public async Task DnsGetHostEntry_LocalhostWithTrailingDot_ReturnsLoopback()
428428
{

0 commit comments

Comments
 (0)