-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Handle RFC 6761 special-use domain names in Dns resolution #123076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
35eb8da
5ecabd0
bf45270
a054182
1d3c480
b0ed820
a223dee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -416,6 +416,71 @@ private static bool ValidateAddressFamily(ref AddressFamily addressFamily, strin | |
| return true; | ||
| } | ||
|
|
||
| private const string Localhost = "localhost"; | ||
| private const string InvalidDomain = "invalid"; | ||
|
|
||
| /// <summary> | ||
| /// Checks if the given host name matches a reserved name or is a subdomain of it. | ||
| /// For example, IsReservedName("foo.localhost", "localhost") returns true. | ||
| /// Also handles trailing dots: IsReservedName("foo.localhost.", "localhost") returns true. | ||
| /// Returns false for malformed hostnames (starting with dot or containing consecutive dots). | ||
| /// </summary> | ||
| private static bool IsReservedName(string hostName, string reservedName) | ||
| { | ||
| // Reject malformed hostnames - let OS resolver handle them (and reject them) | ||
| if (hostName.StartsWith('.') || hostName.Contains("..", StringComparison.Ordinal)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| // Strip trailing dot if present (DNS root notation) | ||
| ReadOnlySpan<char> hostSpan = hostName.AsSpan(); | ||
| if (hostSpan.EndsWith('.')) | ||
| { | ||
| hostSpan = hostSpan.Slice(0, hostSpan.Length - 1); | ||
| } | ||
|
|
||
| // Matches "reservedName" exactly, or "*.reservedName" (subdomain) | ||
| return hostSpan.EndsWith(reservedName, StringComparison.OrdinalIgnoreCase) && | ||
| (hostSpan.Length == reservedName.Length || | ||
| hostSpan[hostSpan.Length - reservedName.Length - 1] == '.'); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if the given host name is a subdomain of localhost (e.g., "foo.localhost"). | ||
| /// Plain "localhost" or "localhost." returns false. | ||
| /// </summary> | ||
| private static bool IsLocalhostSubdomain(string hostName) | ||
| { | ||
| // 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) | ||
| return length > Localhost.Length && IsReservedName(hostName, Localhost); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tries to handle RFC 6761 "invalid" domain names. | ||
| /// Returns true if the host name is an invalid domain (exception will be set). | ||
| /// </summary> | ||
| private static bool TryHandleRfc6761InvalidDomain(string hostName, out SocketException? exception) | ||
| { | ||
| // RFC 6761 Section 6.4: "invalid" and "*.invalid" must always return NXDOMAIN. | ||
| if (IsReservedName(hostName, InvalidDomain)) | ||
| { | ||
| if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Returning NXDOMAIN for 'invalid' domain"); | ||
| exception = new SocketException((int)SocketError.HostNotFound); | ||
| return true; | ||
| } | ||
|
|
||
| exception = null; | ||
| return false; | ||
| } | ||
|
|
||
| private static object GetHostEntryOrAddressesCore(string hostName, bool justAddresses, AddressFamily addressFamily, NameResolutionActivity? activityOrDefault = default) | ||
| { | ||
| ValidateHostName(hostName); | ||
|
|
@@ -429,32 +494,69 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr | |
| // NameResolutionActivity may have already been set if we're being called from RunAsync. | ||
| NameResolutionActivity activity = activityOrDefault ?? NameResolutionTelemetry.Log.BeforeResolution(hostName); | ||
|
|
||
| object result; | ||
| // RFC 6761 Section 6.4: "invalid" domains must return NXDOMAIN. | ||
| if (TryHandleRfc6761InvalidDomain(hostName, out SocketException? invalidDomainException)) | ||
| { | ||
| NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: invalidDomainException); | ||
| throw invalidDomainException!; | ||
| } | ||
|
|
||
| // Track if this is a localhost subdomain for fallback handling. | ||
| bool isLocalhostSubdomain = IsLocalhostSubdomain(hostName); | ||
|
|
||
| bool fallbackToLocalhost = false; | ||
| object? result = null; | ||
| try | ||
| { | ||
| SocketError errorCode = NameResolutionPal.TryGetAddrInfo(hostName, justAddresses, addressFamily, out string? newHostName, out string[] aliases, out IPAddress[] addresses, out int nativeErrorCode); | ||
|
|
||
| if (errorCode != SocketError.Success) | ||
| { | ||
| if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(hostName, $"{hostName} DNS lookup failed with {errorCode}"); | ||
| throw CreateException(errorCode, nativeErrorCode); | ||
| // RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost". | ||
| if (isLocalhostSubdomain) | ||
| { | ||
| 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: CreateException(errorCode, nativeErrorCode)); | ||
| fallbackToLocalhost = true; | ||
| } | ||
| else | ||
| { | ||
| if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(hostName, $"{hostName} DNS lookup failed with {errorCode}"); | ||
| throw CreateException(errorCode, nativeErrorCode); | ||
| } | ||
| } | ||
| else if (addresses.Length == 0 && isLocalhostSubdomain) | ||
| { | ||
| // 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'"); | ||
| NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: justAddresses ? addresses : (object)new IPHostEntry { AddressList = addresses, HostName = newHostName!, Aliases = aliases }, exception: null); | ||
| fallbackToLocalhost = true; | ||
| } | ||
|
|
||
| result = justAddresses ? (object) | ||
| addresses : | ||
| new IPHostEntry | ||
| { | ||
| AddressList = addresses, | ||
| HostName = newHostName!, | ||
| Aliases = aliases | ||
| }; | ||
| if (!fallbackToLocalhost) | ||
| { | ||
| result = justAddresses ? (object) | ||
| addresses : | ||
| new IPHostEntry | ||
| { | ||
| AddressList = addresses, | ||
| HostName = newHostName!, | ||
| Aliases = aliases | ||
| }; | ||
| } | ||
| } | ||
| catch (Exception ex) when (LogFailure(hostName, activity, ex)) | ||
| { | ||
| Debug.Fail("LogFailure should return false"); | ||
| throw; | ||
| } | ||
|
|
||
| if (fallbackToLocalhost) | ||
| { | ||
| return GetHostEntryOrAddressesCore(Localhost, justAddresses, addressFamily); | ||
| } | ||
|
|
||
| Debug.Assert(result is not null); | ||
| NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result); | ||
|
|
||
| return result; | ||
|
|
@@ -588,6 +690,23 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR | |
| } | ||
| else | ||
| { | ||
| // Validate hostname before any processing | ||
| ValidateHostName(hostName); | ||
|
|
||
| // RFC 6761 Section 6.4: "invalid" domains must return NXDOMAIN. | ||
| if (TryHandleRfc6761InvalidDomain(hostName, out SocketException? invalidDomainException)) | ||
| { | ||
| NameResolutionActivity activity = NameResolutionTelemetry.Log.BeforeResolution(hostName); | ||
| NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: invalidDomainException); | ||
| return justAddresses ? (Task) | ||
| Task.FromException<IPAddress[]>(invalidDomainException!) : | ||
| Task.FromException<IPHostEntry>(invalidDomainException!); | ||
| } | ||
|
|
||
| // For localhost subdomains (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. | ||
|
|
||
| if (NameResolutionPal.SupportsGetAddrInfoAsync) | ||
| { | ||
| #pragma warning disable CS0162 // Unreachable code detected -- SupportsGetAddrInfoAsync is a constant on *nix. | ||
|
|
@@ -596,10 +715,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. | ||
|
|
||
| ValidateHostName(hostName); | ||
|
|
||
| // Always use the telemetry-enabled path for localhost subdomains to ensure fallback handling. | ||
| // For other hostnames, use the non-telemetry path if diagnostics are disabled. | ||
| bool isLocalhostSubdomain = IsLocalhostSubdomain(hostName); | ||
| Task? t; | ||
| if (NameResolutionTelemetry.AnyDiagnosticsEnabled()) | ||
| if (NameResolutionTelemetry.AnyDiagnosticsEnabled() || isLocalhostSubdomain) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need the telemetry-enabled path for localhost subdomains? What is preventing us from using the non-telemetry path?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The telemetry-enabled path ( The non-telemetry path ( The condition could perhaps be renamed to be clearer, e.g., |
||
| { | ||
| t = justAddresses | ||
| ? GetAddrInfoWithTelemetryAsync<IPAddress[]>(hostName, justAddresses, family, cancellationToken) | ||
|
|
@@ -653,31 +773,71 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR | |
|
|
||
| if (task != null) | ||
| { | ||
| return CompleteAsync(task, hostName, startingTimestamp); | ||
| bool isLocalhostSubdomain = IsLocalhostSubdomain(hostName); | ||
| return CompleteAsync(task, hostName, justAddresses, addressFamily, isLocalhostSubdomain, 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, long startingTimeStamp) | ||
| static async Task<T> CompleteAsync(Task task, string hostName, bool justAddresses, AddressFamily addressFamily, bool isLocalhostSubdomain, long startingTimeStamp, CancellationToken cancellationToken) | ||
| { | ||
| NameResolutionActivity activity = NameResolutionTelemetry.Log.BeforeResolution(hostName, startingTimeStamp); | ||
| Exception? exception = null; | ||
| T? result = null; | ||
| bool fallbackOccurred = false; | ||
| try | ||
| { | ||
| 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) | ||
| { | ||
| if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'"); | ||
| NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null); | ||
| fallbackOccurred = true; | ||
|
|
||
| // Resolve plain "localhost" instead | ||
| return await ((Task<T>)(justAddresses | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it guaranteed that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No —
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The method is, but the |
||
| ? (Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken) | ||
| : Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken))).ConfigureAwait(false); | ||
| } | ||
|
|
||
| if (isLocalhostSubdomain && 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'"); | ||
| NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null); | ||
| fallbackOccurred = true; | ||
|
|
||
| // Resolve plain "localhost" instead | ||
| return await ((Task<T>)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false); | ||
| } | ||
|
|
||
| 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; | ||
|
|
||
| return await ((Task<T>)(justAddresses | ||
| ? (Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken) | ||
| : Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken))).ConfigureAwait(false); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| exception = ex; | ||
| throw; | ||
| } | ||
| finally | ||
| { | ||
| NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: exception); | ||
| if (!fallbackOccurred) | ||
| { | ||
| NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: exception); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.