From 6fdaf5cef90f5390f960620646fed527dc71f35f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 09:29:13 +0100 Subject: [PATCH 1/2] Fix DNS localhost subdomain resolution with IPv6 on Android On Android, getaddrinfo("localhost", AF_INET6) fails with EAI_NONAME because the default /etc/hosts maps ::1 to "ip6-localhost" instead of "localhost". This causes the RFC 6761 localhost subdomain fallback to fail when InterNetworkV6 is requested. Add an Android-specific catch in the fallback path: when IPv6 localhost resolution fails with HostNotFound, retry with "ip6-localhost". Fixes #124751 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Net/Dns.cs | 56 ++++++++++++++++--- .../FunctionalTests/GetHostAddressesTest.cs | 1 - .../tests/FunctionalTests/GetHostEntryTest.cs | 1 - 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs index 67c29760dea20b..fcc4a8c4393083 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs @@ -419,6 +419,10 @@ private static bool ValidateAddressFamily(ref AddressFamily addressFamily, strin private const string Localhost = "localhost"; private const string InvalidDomain = "invalid"; + // Android's default /etc/hosts maps ::1 to "ip6-localhost" instead of "localhost", + // which causes getaddrinfo("localhost", AF_INET6) to fail with EAI_NONAME. + private const string AndroidIPv6Localhost = "ip6-localhost"; + /// /// Checks if the given host name matches a reserved name or is a subdomain of it. /// For example, IsReservedName("foo.localhost", "localhost") returns true. @@ -550,7 +554,16 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr if (fallbackToLocalhost) { - return GetHostEntryOrAddressesCore(Localhost, justAddresses, addressFamily); + try + { + return GetHostEntryOrAddressesCore(Localhost, justAddresses, addressFamily); + } + catch (SocketException ex) when (OperatingSystem.IsAndroid() && addressFamily == AddressFamily.InterNetworkV6 && ex.SocketErrorCode == SocketError.HostNotFound) + { + // Android's default /etc/hosts maps ::1 to "ip6-localhost" instead of "localhost". + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(Localhost, $"localhost resolution with IPv6 failed on Android, retrying with '{AndroidIPv6Localhost}'"); + return GetHostEntryOrAddressesCore(AndroidIPv6Localhost, justAddresses, addressFamily); + } } Debug.Assert(result is not null); @@ -795,8 +808,7 @@ static async Task CompleteAsync(Task task, string hostName, bool justAddresse NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null); fallbackOccurred = true; - // result is IPAddress[] so justAddresses is guaranteed true here. - return await ((Task)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false); + return await GetLocalhostAddressesAsync(addressFamily, cancellationToken).ConfigureAwait(false); } if (isLocalhostSubdomain && result is IPHostEntry entry && entry.AddressList.Length == 0) @@ -805,8 +817,7 @@ static async Task CompleteAsync(Task task, string hostName, bool justAddresse NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null); fallbackOccurred = true; - // result is IPHostEntry so justAddresses is guaranteed false here. - return await ((Task)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false); + return await GetLocalhostEntryAsync(addressFamily, cancellationToken).ConfigureAwait(false); } return result; @@ -818,9 +829,9 @@ static async Task CompleteAsync(Task task, string hostName, bool justAddresse NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex); fallbackOccurred = true; - return await ((Task)(justAddresses - ? (Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken) - : Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken))).ConfigureAwait(false); + return justAddresses + ? await GetLocalhostAddressesAsync(addressFamily, cancellationToken).ConfigureAwait(false) + : await GetLocalhostEntryAsync(addressFamily, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -834,6 +845,35 @@ static async Task CompleteAsync(Task task, string hostName, bool justAddresse NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: exception); } } + + // Resolves "localhost" with the given address family, returning addresses. + // On Android, if IPv6 resolution fails with HostNotFound, retries with "ip6-localhost" + // because Android's default /etc/hosts maps ::1 to "ip6-localhost" instead of "localhost". + static async Task GetLocalhostAddressesAsync(AddressFamily family, CancellationToken cancellationToken) + { + try + { + return await ((Task)(Task)Dns.GetHostAddressesAsync(Localhost, family, cancellationToken)).ConfigureAwait(false); + } + catch (SocketException ex) when (OperatingSystem.IsAndroid() && family == AddressFamily.InterNetworkV6 && ex.SocketErrorCode == SocketError.HostNotFound) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(Localhost, $"localhost resolution with IPv6 failed on Android, retrying with '{AndroidIPv6Localhost}'"); + return await ((Task)(Task)Dns.GetHostAddressesAsync(AndroidIPv6Localhost, family, cancellationToken)).ConfigureAwait(false); + } + } + + static async Task GetLocalhostEntryAsync(AddressFamily family, CancellationToken cancellationToken) + { + try + { + return await ((Task)(Task)Dns.GetHostEntryAsync(Localhost, family, cancellationToken)).ConfigureAwait(false); + } + catch (SocketException ex) when (OperatingSystem.IsAndroid() && family == AddressFamily.InterNetworkV6 && ex.SocketErrorCode == SocketError.HostNotFound) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(Localhost, $"localhost resolution with IPv6 failed on Android, retrying with '{AndroidIPv6Localhost}'"); + return await ((Task)(Task)Dns.GetHostEntryAsync(AndroidIPv6Localhost, family, cancellationToken)).ConfigureAwait(false); + } + } } } diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs index e98abe1ab53e2f..b91219cf6fed19 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs @@ -249,7 +249,6 @@ public async Task DnsGetHostAddresses_LocalhostSubdomain_ReturnsLoopback(string [Theory] [InlineData(AddressFamily.InterNetwork)] [InlineData(AddressFamily.InterNetworkV6)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124751", TestPlatforms.Android)] public async Task DnsGetHostAddresses_LocalhostSubdomain_RespectsAddressFamily(AddressFamily addressFamily) { // Skip IPv6 test if OS doesn't support it. diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs index 58c8015a50bd70..e3cb7066eadc17 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs @@ -414,7 +414,6 @@ public async Task DnsGetHostEntry_LocalhostWithTrailingDot_ReturnsLoopback() [Theory] [InlineData(AddressFamily.InterNetwork)] [InlineData(AddressFamily.InterNetworkV6)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124751", TestPlatforms.Android)] public async Task DnsGetHostEntry_LocalhostSubdomain_RespectsAddressFamily(AddressFamily addressFamily) { // Skip IPv6 test if OS doesn't support it. From 57952770589e7bf1b14f5bda1d8d155f574fc50f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 13 Mar 2026 12:16:30 +0100 Subject: [PATCH 2/2] Generalize ip6-localhost fallback to all platforms Remove the OperatingSystem.IsAndroid() guard so the ip6-localhost IPv6 fallback applies on any system where /etc/hosts maps ::1 to ip6-localhost instead of localhost (e.g. Android, some Linux distros). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Net/Dns.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs index fcc4a8c4393083..fdac620bbf15df 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs @@ -419,9 +419,9 @@ private static bool ValidateAddressFamily(ref AddressFamily addressFamily, strin private const string Localhost = "localhost"; private const string InvalidDomain = "invalid"; - // Android's default /etc/hosts maps ::1 to "ip6-localhost" instead of "localhost", - // which causes getaddrinfo("localhost", AF_INET6) to fail with EAI_NONAME. - private const string AndroidIPv6Localhost = "ip6-localhost"; + // Some systems (e.g. Android, some Linux distros) map ::1 to "ip6-localhost" instead of + // "localhost" in /etc/hosts, which causes getaddrinfo("localhost", AF_INET6) to fail with EAI_NONAME. + private const string IPv6Localhost = "ip6-localhost"; /// /// Checks if the given host name matches a reserved name or is a subdomain of it. @@ -558,11 +558,11 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr { return GetHostEntryOrAddressesCore(Localhost, justAddresses, addressFamily); } - catch (SocketException ex) when (OperatingSystem.IsAndroid() && addressFamily == AddressFamily.InterNetworkV6 && ex.SocketErrorCode == SocketError.HostNotFound) + catch (SocketException ex) when (addressFamily == AddressFamily.InterNetworkV6 && ex.SocketErrorCode == SocketError.HostNotFound) { - // Android's default /etc/hosts maps ::1 to "ip6-localhost" instead of "localhost". - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(Localhost, $"localhost resolution with IPv6 failed on Android, retrying with '{AndroidIPv6Localhost}'"); - return GetHostEntryOrAddressesCore(AndroidIPv6Localhost, justAddresses, addressFamily); + // Some systems map ::1 to "ip6-localhost" instead of "localhost" in /etc/hosts. + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(Localhost, $"localhost IPv6 resolution failed, retrying with '{IPv6Localhost}'"); + return GetHostEntryOrAddressesCore(IPv6Localhost, justAddresses, addressFamily); } } @@ -847,18 +847,18 @@ static async Task CompleteAsync(Task task, string hostName, bool justAddresse } // Resolves "localhost" with the given address family, returning addresses. - // On Android, if IPv6 resolution fails with HostNotFound, retries with "ip6-localhost" - // because Android's default /etc/hosts maps ::1 to "ip6-localhost" instead of "localhost". + // If IPv6 resolution fails with HostNotFound, retries with "ip6-localhost" + // because some systems map ::1 to "ip6-localhost" instead of "localhost" in /etc/hosts. static async Task GetLocalhostAddressesAsync(AddressFamily family, CancellationToken cancellationToken) { try { return await ((Task)(Task)Dns.GetHostAddressesAsync(Localhost, family, cancellationToken)).ConfigureAwait(false); } - catch (SocketException ex) when (OperatingSystem.IsAndroid() && family == AddressFamily.InterNetworkV6 && ex.SocketErrorCode == SocketError.HostNotFound) + catch (SocketException ex) when (family == AddressFamily.InterNetworkV6 && ex.SocketErrorCode == SocketError.HostNotFound) { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(Localhost, $"localhost resolution with IPv6 failed on Android, retrying with '{AndroidIPv6Localhost}'"); - return await ((Task)(Task)Dns.GetHostAddressesAsync(AndroidIPv6Localhost, family, cancellationToken)).ConfigureAwait(false); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(Localhost, $"localhost IPv6 resolution failed, retrying with '{IPv6Localhost}'"); + return await ((Task)(Task)Dns.GetHostAddressesAsync(IPv6Localhost, family, cancellationToken)).ConfigureAwait(false); } } @@ -868,10 +868,10 @@ static async Task GetLocalhostEntryAsync(AddressFamily family, CancellationTo { return await ((Task)(Task)Dns.GetHostEntryAsync(Localhost, family, cancellationToken)).ConfigureAwait(false); } - catch (SocketException ex) when (OperatingSystem.IsAndroid() && family == AddressFamily.InterNetworkV6 && ex.SocketErrorCode == SocketError.HostNotFound) + catch (SocketException ex) when (family == AddressFamily.InterNetworkV6 && ex.SocketErrorCode == SocketError.HostNotFound) { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(Localhost, $"localhost resolution with IPv6 failed on Android, retrying with '{AndroidIPv6Localhost}'"); - return await ((Task)(Task)Dns.GetHostEntryAsync(AndroidIPv6Localhost, family, cancellationToken)).ConfigureAwait(false); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(Localhost, $"localhost IPv6 resolution failed, retrying with '{IPv6Localhost}'"); + return await ((Task)(Task)Dns.GetHostEntryAsync(IPv6Localhost, family, cancellationToken)).ConfigureAwait(false); } } }