Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 32 additions & 25 deletions src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ private static bool ValidateAddressFamily(ref AddressFamily addressFamily, strin
}

private const string Localhost = "localhost";
private const string LocalhostWithTrailingDot = Localhost + ".";
private const string InvalidDomain = "invalid";

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

/// <summary>
/// Checks if the given host name is a subdomain of localhost (e.g., "foo.localhost").
/// Plain "localhost" or "localhost." returns false.
/// Checks if the given host name should fall back to "localhost" when resolution fails or returns no addresses.
/// Fully qualified "localhost." and localhost subdomains return true; plain "localhost" returns false.
/// </summary>
private static bool IsLocalhostSubdomain(string hostName)
private static bool ShouldFallbackToLocalhost(string hostName)
{
// Plain "localhost" is the fallback target, so only its fully qualified form should enter this path.
if (hostName.Equals(LocalhostWithTrailingDot, StringComparison.OrdinalIgnoreCase))
{
return true;
}

// Strip trailing dot for length comparison
int length = hostName.Length;
if (hostName.EndsWith('.'))
{
length--;
}

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

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

if (errorCode != SocketError.Success)
{
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
if (IsLocalhostSubdomain(hostName))
// RFC 6761 Section 6.3: If a localhost name fails, fall back to resolving plain "localhost".
if (ShouldFallbackToLocalhost(hostName))
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost name resolution failed, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: CreateException(errorCode, nativeErrorCode));
fallbackToLocalhost = true;
}
Expand All @@ -526,10 +533,10 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr
throw CreateException(errorCode, nativeErrorCode);
}
}
else if (addresses.Length == 0 && IsLocalhostSubdomain(hostName))
else if (addresses.Length == 0 && ShouldFallbackToLocalhost(hostName))
{
// RFC 6761 Section 6.3: If localhost subdomain returns empty addresses, fall back to plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'");
// RFC 6761 Section 6.3: If a localhost name returns empty addresses, fall back to plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost name returned empty, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: justAddresses ? addresses : (object)new IPHostEntry { AddressList = addresses, HostName = newHostName!, Aliases = aliases }, exception: null);
fallbackToLocalhost = true;
}
Expand Down Expand Up @@ -713,7 +720,7 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR
Task.FromException<IPHostEntry>(invalidDomainException!);
}

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

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

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

if (task != null)
{
bool isLocalhostSubdomain = IsLocalhostSubdomain(hostName);
return CompleteAsync(task, hostName, justAddresses, addressFamily, isLocalhostSubdomain, startingTimestamp, cancellationToken);
bool shouldFallbackToLocalhost = ShouldFallbackToLocalhost(hostName);
return CompleteAsync(task, hostName, justAddresses, addressFamily, shouldFallbackToLocalhost, startingTimestamp, cancellationToken);
}

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

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

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

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

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

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

return result;
}
catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
catch (SocketException ex) when (shouldFallbackToLocalhost && !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'");
// RFC 6761 Section 6.3: If a localhost name fails, fall back to resolving plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost name resolution failed, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
fallbackOccurred = true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ public async Task DnsGetHostAddresses_MalformedReservedName_NotTreatedAsReserved
await Assert.ThrowsAnyAsync<Exception>(() => Dns.GetHostAddressesAsync(hostName));
}

// "localhost." (with trailing dot) should NOT be treated as a subdomain.
// "localhost." is resolved by the OS first, with fallback to plain "localhost" on failure or an empty result.
[Fact]
public async Task DnsGetHostAddresses_LocalhostWithTrailingDot_ReturnsLoopback()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,8 @@ public async Task DnsGetHostEntry_MalformedReservedName_NotTreatedAsReserved(str
await Assert.ThrowsAnyAsync<Exception>(() => Dns.GetHostEntryAsync(hostName));
}

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