Skip to content

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses#128068

Open
kotlarmilos wants to merge 6 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback
Open

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses#128068
kotlarmilos wants to merge 6 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback

Conversation

@kotlarmilos
Copy link
Copy Markdown
Member

@kotlarmilos kotlarmilos commented May 12, 2026

Description

Dns falls back to resolving plain localhost when the OS resolver fails or returns zero addresses for a *.localhost subdomain (RFC 6761 §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. The same behavior was observed on iOS/tvOS/MacCatalyst. 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.

Six previously-disabled tests covering the same root cause are re-enabled in this PR.

Fixes #127965.
Fixes #127953.

Supersedes the test-skip workarounds:

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@kotlarmilos kotlarmilos force-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from b7205f9 to 3a51363 Compare May 12, 2026 08:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Net.NameResolution DNS resolution behavior for *.localhost (RFC 6761 §6.3) to avoid returning non-loopback addresses on Android when the platform resolver provides unexpected results, by falling back to resolving plain localhost in more cases.

Changes:

  • Add a helper to detect when *.localhost results contain no loopback addresses.
  • Extend the RFC 6761 §6.3 fallback logic in the synchronous resolution path to trigger when the OS returns only non-loopback addresses.
  • Extend the same fallback logic in the async (GetAddrInfoAsync) completion path for both IPAddress[] and IPHostEntry.
Show a summary per file
File Description
src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Extends RFC 6761 localhost-subdomain fallback logic to handle non-loopback OS resolver results (sync + async).

Copilot's findings

Comments suppressed due to low confidence (3)

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

  • The new RFC 6761 fallback condition only triggers when the OS returns zero loopback addresses (HasNoLoopbackAddress). However the failing Android test report shows a mixed result set ("7 out of 8 items" non-loopback), which would bypass this fallback and still return non-loopback addresses. Consider treating any non-loopback address in a *.localhost result as grounds to discard/filter the OS result (e.g., fall back when not all returned addresses are loopback, or filter the list to loopback addresses only).
                    fallbackToLocalhost = true;
                }

                if (!fallbackToLocalhost)
                {
                    result = justAddresses ? (object)
                        addresses :
                        new IPHostEntry

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

  • Same issue in the async completion path: the fallback only triggers when there are no loopback addresses at all, so a mixed address list (some loopback + some non-loopback) will not be corrected and can still violate RFC 6761 and the existing loopback-only test. Update the condition/logic to handle mixed results (discard or filter out non-loopback addresses for *.localhost).
                        // result is IPAddress[] so justAddresses is guaranteed true here.
                        return await ((Task<T>)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);
                    }

                    if (isLocalhostSubdomain && result is IPHostEntry entry &&
                        (entry.AddressList.Length == 0 || HasNoLoopbackAddress(entry.AddressList)))
                    {
                        if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned no loopback addresses, falling back to 'localhost'");
                        NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
                        fallbackOccurred = true;

                        // result is IPHostEntry so justAddresses is guaranteed false here.
                        return await ((Task<T>)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);

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

  • The IPHostEntry async fallback path has the same mixed-result problem: if entry.AddressList contains at least one loopback plus additional non-loopback addresses, HasNoLoopbackAddress returns false and the method returns the OS result unchanged. To satisfy RFC 6761 and keep behavior consistent with the address-array path, consider falling back (or filtering) when the result contains any non-loopback addresses.

                    return result;
                }
                catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
                {
                    // RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
                    if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
                    NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
                    fallbackOccurred = true;

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmilos kotlarmilos requested review from liveans and rzikm May 12, 2026 08:58
@kotlarmilos kotlarmilos added this to the 11.0.0 milestone May 12, 2026
kotlarmilos and others added 2 commits May 12, 2026 12:43
…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 dotnet#127965.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous [ActiveIssue] skips on six DnsGetHostAddresses_/DnsGetHostEntry_LocalhostSubdomain* tests (referencing dotnet#126456 and dotnet#127965) all covered the same root cause: platform resolvers (Android bionic, iOS/tvOS/MacCatalyst) returned non-loopback addresses for '*.localhost' subdomains, bypassing the existing RFC 6761 6.3 fallback. With the fallback now extended to trigger when the OS returns no loopback addresses, these tests are expected to pass on every platform.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos kotlarmilos force-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from 3a51363 to a536036 Compare May 12, 2026 10:43
@kotlarmilos kotlarmilos marked this pull request as ready for review May 12, 2026 13:55
Copilot AI review requested due to automatic review settings May 12, 2026 13:55
@kotlarmilos
Copy link
Copy Markdown
Member Author

/azp run runtime-extra-platforms

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment thread src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
…Address

Address review feedback: HasNoLoopbackAddress previously returned false
for an empty array, forcing every caller to spell the condition as
'Length == 0 || HasNoLoopbackAddress(...)'. An empty result has no
loopback addresses by definition, so the helper now returns true in
that case, and the three call sites are simplified accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
Copy link
Copy Markdown
Member Author

/azp run runtime-extra-platforms

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

Copilot AI review requested due to automatic review settings May 13, 2026 11:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment thread src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
Comment thread src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
Comment thread src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
@kotlarmilos
Copy link
Copy Markdown
Member Author

@copilot look at the comment

I think the current behavior is correct, if user sets non-loopback addresses in /etc/hosts for e.g. dev.localhost, then we should respect that. If there are tests that fail due to specific platform behavior, then we should adjust the test expectations

Per review feedback: the Dns.cs behavior is correct, /etc/hosts non-loopback
entries for *.localhost should be respected. Revert the production code to
only fall back to plain 'localhost' when the OS resolver returns empty.

Adapt the six re-enabled tests to skip the IsLoopback assertion on Android
and Apple mobile (iOS, tvOS, MacCatalyst), where the OS resolver may legitimately
return non-loopback addresses (e.g. link-local IPv6) for *.localhost.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos kotlarmilos requested a review from rzikm May 14, 2026 11:00
Copy link
Copy Markdown
Member

@rzikm rzikm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM if tests pass

@kotlarmilos
Copy link
Copy Markdown
Member Author

/azp run runtime-extra-platforms

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants