Skip to content

Conversation

@laveeshb
Copy link
Contributor

@laveeshb laveeshb commented Jan 11, 2026

Handle RFC 6761 special-use domain names in Dns resolution

Description

This PR implements RFC 6761 compliant handling for special-use domain names in System.Net.Dns.

RFC 6761 compliance:

  • invalid and *.invalid - Return SocketError.HostNotFound (NXDOMAIN) per Section 6.4
  • *.localhost subdomains - Try OS resolver first, fall back to plain localhost resolution if OS returns failure or empty (per Section 6.3)
  • Plain localhost - Continues to use OS resolver to preserve /etc/hosts customizations

Behavior Change

Before this PR:

  • Dns.GetHostAddresses("test.invalid") - Would query DNS servers and eventually fail
  • Dns.GetHostAddresses("foo.localhost") - Would fail with HostNotFound (most systems don't have subdomains configured)

After this PR:

  • Dns.GetHostAddresses("test.invalid") - Immediately returns SocketError.HostNotFound without network I/O
  • Dns.GetHostAddresses("foo.localhost") - Tries OS resolver first, falls back to returning loopback addresses if OS fails/returns empty

This is an intentional breaking change for RFC 6761 compliance. Applications that previously caught SocketException for *.localhost subdomains will now get successful results (loopback addresses). This enables local development scenarios where services use *.localhost subdomains.

Implementation Notes

Updated based on team feedback from @rzikm:

The previous implementation returned pre-allocated loopback arrays immediately for *.localhost subdomains. This was changed to:

  1. Try OS resolver first - Allows systems with custom localhost subdomain configurations (e.g., in /etc/hosts or local DNS) to work correctly
  2. Fall back to plain localhost resolution - If OS resolver fails or returns empty results, resolve plain localhost instead to guarantee loopback addresses

This approach:

  • Respects system-level customizations
  • Returns the same addresses as localhost on typical systems
  • Preserves IPv4/IPv6 preference from OS resolver
  • Works correctly with AddressFamily filtering

Testing

Added tests covering:

  • Invalid domain rejection (invalid, test.invalid, foo.bar.invalid, invalid., test.invalid.)
  • Localhost subdomain resolution (foo.localhost, bar.foo.localhost, foo.localhost.)
  • Case-insensitivity (INVALID, Test.INVALID, FOO.LOCALHOST)
  • AddressFamily filtering for localhost subdomains
  • Trailing dot handling (DNS root notation)
  • Edge cases like notlocalhost, localhostfoo to ensure similar names aren't incorrectly matched
  • Malformed names (.localhost, ..invalid) passed to OS resolver
  • Verification that localhost subdomains return same addresses as plain localhost

Fixes #118569

Copilot AI review requested due to automatic review settings January 11, 2026 18:09
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Jan 11, 2026
@dotnet-policy-service
Copy link
Contributor

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

Copy link
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 implements RFC 6761 compliant handling for special-use domain names in System.Net.Dns. The implementation adds logic to intercept DNS queries for invalid/*.invalid domains (returning NXDOMAIN) and *.localhost subdomains (returning loopback addresses) before they reach the OS resolver. Plain localhost continues to use the OS resolver to preserve /etc/hosts customizations.

Changes:

  • Added RFC 6761 special-use domain name handling to DNS resolution with proper telemetry integration
  • Implemented helper methods for efficient string matching and loopback address allocation
  • Added comprehensive test coverage for invalid domains, localhost subdomains, and address family filtering

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Added helper methods (IsReservedName, GetLoopbackAddresses, TryHandleRfc6761SpecialName) and integrated RFC 6761 handling into both sync and async resolution paths
src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs Added tests for invalid domain rejection, localhost subdomain resolution, address family filtering, and edge cases
src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs Added parallel tests for GetHostAddresses API covering invalid domains and localhost subdomains

// Matches "reservedName" exactly, or "*.reservedName" (subdomain)
return hostName.EndsWith(reservedName, StringComparison.OrdinalIgnoreCase) &&
(hostName.Length == reservedName.Length ||
hostName[hostName.Length - reservedName.Length - 1] == '.');
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

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

The IsReservedName function has a bug with edge case hostnames starting with a dot. For example, ".localhost" or ".invalid" would incorrectly match because the function checks if the character before the reserved name is a dot, but doesn't verify there's actual content before that dot. This would cause ".localhost" to be treated as a reserved name when it shouldn't be.

To fix this, add a check to ensure the dot is not at position 0. The condition should be:
hostName[hostName.Length - reservedName.Length - 1] == '.' && hostName.Length > reservedName.Length + 1

Or alternatively, check that the position of the dot is greater than 0:
(hostName.Length - reservedName.Length - 1) > 0 && hostName[hostName.Length - reservedName.Length - 1] == '.'

Suggested change
hostName[hostName.Length - reservedName.Length - 1] == '.');
(hostName.Length > reservedName.Length + 1 &&
hostName[hostName.Length - reservedName.Length - 1] == '.'));

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

hmm, this brings up more questions. We probably don't want to return loopback address for cases like:

.localhost
.something.localhost
something..localhost
so.me..thing.localhost

as they are all malformed, but validating that a string is a valid hostname is nontrivial (label lengths, punycode....) and we don't have that implemented anywhere IIRC. @wfurt any thoughts here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add validation to reject malformed hostnames (those starting with ., containing .., or ending with .) and let them fall through to the OS resolver.

Copy link
Member

Choose a reason for hiding this comment

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

or ending with .

Ending with . is valid (i.e. localhost and localhost. are equivalent) the empty label at the end is the root label which does not need to be explicitly stated in the API (but is always part of the encoded query), but that is not necessarily what I meant. I am not sure we should implement partial hostname validation.

If any, we should implement complete hostname validation with all it's edgecases (label length restrictions, total length restriction, punycode encoding, etc.), which is complicated and I suggest you wait a bit while we discuss in the team whether that is something that we want.

Copy link
Member

Choose a reason for hiding this comment

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

We've discussed this in the team, and have consensus on the following

  • we want to preserve the existing behavior for folks who are using /etc/hosts.conf to redirect localhost subdomains to specific IPs. So 'attempting to resolve those subdomains in the native resolver first, and if resolves to empty set of addresses, then falling back to whatever "localhost" resolves to' seems a decent middle ground.
  • there is no need to implement exhaustive hostname validation, rejecting just obviously malformed names should be enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! The implementation now rejects obviously malformed hostnames (starting with . or containing ..) and passes them to the OS resolver, which will reject them appropriately. I'm not doing exhaustive hostname validation as discussed.

}

// RFC 6761 Section 6.3: "*.localhost" subdomains must resolve to loopback.
// Note: Plain "localhost" is handled by the OS resolver (preserves /etc/hosts customizations).
Copy link
Member

Choose a reason for hiding this comment

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

It feels weird to allow customizations for "localhost" name itself, but not for subdomains.

It also means that without any customization, names "localhost" and "a.localhost" may return different results (e.g. IPv4 and IPv6 addresses in different order)

Copy link
Contributor Author

@laveeshb laveeshb Jan 13, 2026

Choose a reason for hiding this comment

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

You're right that this creates an inconsistency where localhost and a.localhost may return different results.

The current approach defers plain \localhost\ to the OS resolver intentionally — handling it in code would be a breaking change for users with custom hosts file entries (e.g., mapping localhost to a specific IP).

Handling both identically would be more RFC 6761 compliant and provide consistent behavior, but at the cost of backward compatibility.

My preference would be to avoid the breaking change, but I'm not sure what the general practice is in dotnet for situations like this. Is there guidance on when RFC compliance should take precedence over backward compatibility?

Copy link
Member

Choose a reason for hiding this comment

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

what if we strip the subdomains and return the same result as simply querying "localhost"? that would preserve the existing behavior and in majority of the cases the subdomains would be resolved to loopback addresses anyway (and we no longer need to care about whether IPv4 or IPv6 is enabled as that would be handled by the native layer)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done - this is exactly what the updated implementation does.

@rzikm rzikm requested a review from a team January 12, 2026 11:09
Implement RFC 6761 compliant handling for special-use domain names:

- 'invalid' and '*.invalid' domains return SocketError.HostNotFound (NXDOMAIN)
- '*.localhost' subdomains resolve to loopback addresses (127.0.0.1 and/or ::1)
- Plain 'localhost' continues to use OS resolver to preserve /etc/hosts customizations

Implementation details:
- Pre-allocated loopback address arrays to avoid allocations on hot path
- Respects AddressFamily parameter for filtered results
- Includes telemetry for diagnostics consistency
- Case-insensitive matching per RFC specification

Added tests for invalid domains, localhost subdomains, AddressFamily filtering,
and edge cases like 'notlocalhost' to ensure similar names aren't incorrectly matched.
- Fix IPv6 address order: use [IPv6Loopback, Loopback] to match Windows resolver behavior
- Add validation to reject malformed hostnames (starting with '.' or containing '..') in IsReservedName, letting them fall through to OS resolver
- Add tests for malformed hostname edge cases (.localhost, foo..localhost, .invalid, test..invalid)
@laveeshb laveeshb force-pushed the fix/rfc6761-special-domains branch from 84b6b50 to 8fbc05e Compare January 13, 2026 07:33
@rzikm rzikm self-assigned this Jan 13, 2026
- RFC 6761 Section 6.4: 'invalid' and '*.invalid' domains immediately return NXDOMAIN
- RFC 6761 Section 6.3: '*.localhost' subdomains try OS resolver first, fall back to plain 'localhost' resolution if OS returns failure or empty results
- Handles trailing dots (DNS root notation) correctly
- Rejects malformed hostnames (starting with '.' or containing '..') by passing to OS resolver
- Comprehensive test coverage for all edge cases
@laveeshb
Copy link
Contributor Author

Thanks for the feedback @rzikm! I've updated the implementation based on your suggestion:

Changes made:

  • For *.localhost\ subdomains, the code now tries the OS resolver first
  • If the OS resolver fails (returns error) or returns empty results, it falls back to resolving plain \localhost\
  • This preserves any system-level customizations (e.g., entries in /etc/hosts\ or local DNS) while ensuring RFC 6761 compliance as a fallback

Benefits of this approach:

  1. Systems that have custom localhost subdomain configurations will get those results
  2. On typical systems (no custom config), subdomains get the same addresses as \localhost\
  3. The \AddressFamily\ filter is properly respected through the fallback
  4. Telemetry is properly logged for both the original resolution and any fallback

The \invalid\ domain handling remains unchanged - those immediately return NXDOMAIN without hitting the OS resolver, as per RFC 6761 Section 6.4.

All 133 tests pass, including new tests that verify localhost subdomains return the same addresses as plain \localhost.

@laveeshb
Copy link
Contributor Author

Correction to formatting above:

Thanks for the feedback @rzikm! I've updated the implementation based on your suggestion:

Changes made:

  • For *.localhost subdomains, the code now tries the OS resolver first
  • If the OS resolver fails (returns error) or returns empty results, it falls back to resolving plain localhost
  • This preserves any system-level customizations (e.g., entries in /etc/hosts or local DNS) while ensuring RFC 6761 compliance as a fallback

Benefits of this approach:

  1. Systems that have custom localhost subdomain configurations will get those results
  2. On typical systems (no custom config), subdomains get the same addresses as localhost
  3. The AddressFamily filter is properly respected through the fallback
  4. Telemetry is properly logged for both the original resolution and any fallback

The invalid domain handling remains unchanged - those immediately return NXDOMAIN without hitting the OS resolver, as per RFC 6761 Section 6.4.

All 133 tests pass, including new tests that verify localhost subdomains return the same addresses as plain localhost.

bool isLocalhostSubdomain = IsLocalhostSubdomain(hostName);
Task? t;
if (NameResolutionTelemetry.AnyDiagnosticsEnabled())
if (NameResolutionTelemetry.AnyDiagnosticsEnabled() || isLocalhostSubdomain)
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The telemetry-enabled path (GetAddrInfoWithTelemetryAsync) contains the localhost subdomain fallback logic - if the OS resolver returns an empty address list or throws a SocketException, it falls back to resolving plain localhost instead. This is the RFC 6761 Section 6.3 compliant behavior for *.localhost subdomains.

The non-telemetry path (NameResolutionPal.GetAddrInfoAsync) doesn't have this fallback handling - it just returns whatever the OS resolver returns. So we need to route localhost subdomains through the telemetry-enabled path to ensure the fallback mechanism is invoked when needed.

The condition could perhaps be renamed to be clearer, e.g., GetAddrInfoWithFallbackAsync or add a comment explaining that the method handles both telemetry and RFC 6761 fallback. Would that help clarify the intent?

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

Labels

area-System.Net community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Domain name resolution does not handle RFC6761 special names correctly

2 participants