Skip to content

Commit b7205f9

Browse files
kotlarmilosCopilot
andcommitted
[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses
Dns falls back to resolving plain 'localhost' when the OS resolver fails or returns zero addresses for a '*.localhost' subdomain (RFC 6761 Section 6.3). However, on Android the bionic getaddrinfo returns non-loopback addresses (link-local fe80::* and globally-routable IPv6) for '*.localhost', bypassing the fallback and causing Dns.GetHostAddresses("foo.localhost.") to return non-loopback addresses. This was caused by the fallback condition only triggering on empty or failed OS responses; it is now extended to also trigger when the OS returns only non-loopback addresses, in both the sync and async paths. Fixes #127965. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5001116 commit b7205f9

1 file changed

Lines changed: 39 additions & 8 deletions

File tree

  • src/libraries/System.Net.NameResolution/src/System/Net

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

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,30 @@ private static bool IsLocalhostSubdomain(string hostName)
463463
return length > Localhost.Length && IsReservedName(hostName, Localhost);
464464
}
465465

466+
/// <summary>
467+
/// Returns true when the address array is non-empty and contains no loopback addresses.
468+
/// Used by the RFC 6761 Section 6.3 fallback path: some platform resolvers (e.g. Android's
469+
/// bionic getaddrinfo) return non-loopback addresses for "*.localhost" subdomains. In that
470+
/// case we discard the OS result and fall back to resolving plain "localhost".
471+
/// </summary>
472+
private static bool HasNoLoopbackAddress(IPAddress[] addresses)
473+
{
474+
if (addresses.Length == 0)
475+
{
476+
return false;
477+
}
478+
479+
foreach (IPAddress address in addresses)
480+
{
481+
if (IPAddress.IsLoopback(address))
482+
{
483+
return false;
484+
}
485+
}
486+
487+
return true;
488+
}
489+
466490
/// <summary>
467491
/// Tries to handle RFC 6761 "invalid" domain names.
468492
/// Returns true if the host name is an invalid domain (exception will be set).
@@ -522,10 +546,13 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr
522546
throw CreateException(errorCode, nativeErrorCode);
523547
}
524548
}
525-
else if (addresses.Length == 0 && IsLocalhostSubdomain(hostName))
549+
else if (IsLocalhostSubdomain(hostName) &&
550+
(addresses.Length == 0 || HasNoLoopbackAddress(addresses)))
526551
{
527-
// RFC 6761 Section 6.3: If localhost subdomain returns empty addresses, fall back to plain "localhost".
528-
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'");
552+
// RFC 6761 Section 6.3: If localhost subdomain returns empty addresses, or only
553+
// non-loopback addresses (e.g. Android bionic returns globally-routable IPv6 for
554+
// "*.localhost"), fall back to plain "localhost".
555+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned no loopback addresses, falling back to 'localhost'");
529556
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: justAddresses ? addresses : (object)new IPHostEntry { AddressList = addresses, HostName = newHostName!, Aliases = aliases }, exception: null);
530557
fallbackToLocalhost = true;
531558
}
@@ -788,20 +815,24 @@ static async Task<T> CompleteAsync(Task task, string hostName, bool justAddresse
788815
{
789816
result = await ((Task<T>)task).ConfigureAwait(false);
790817

791-
// RFC 6761 Section 6.3: If localhost subdomain returns empty addresses, fall back to plain "localhost".
792-
if (isLocalhostSubdomain && result is IPAddress[] addresses && addresses.Length == 0)
818+
// RFC 6761 Section 6.3: If localhost subdomain returns no loopback addresses
819+
// (empty, or only non-loopback like Android bionic's globally-routable IPv6),
820+
// fall back to plain "localhost".
821+
if (isLocalhostSubdomain && result is IPAddress[] addresses &&
822+
(addresses.Length == 0 || HasNoLoopbackAddress(addresses)))
793823
{
794-
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'");
824+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned no loopback addresses, falling back to 'localhost'");
795825
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
796826
fallbackOccurred = true;
797827

798828
// result is IPAddress[] so justAddresses is guaranteed true here.
799829
return await ((Task<T>)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);
800830
}
801831

802-
if (isLocalhostSubdomain && result is IPHostEntry entry && entry.AddressList.Length == 0)
832+
if (isLocalhostSubdomain && result is IPHostEntry entry &&
833+
(entry.AddressList.Length == 0 || HasNoLoopbackAddress(entry.AddressList)))
803834
{
804-
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'");
835+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned no loopback addresses, falling back to 'localhost'");
805836
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
806837
fallbackOccurred = true;
807838

0 commit comments

Comments
 (0)